diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 1274744c6..35d5261da 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -34,7 +35,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable }; [JsonIgnore] - private string configPath; + private string? configPath; [JsonIgnore] private bool isSaveQueued; @@ -48,12 +49,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Event that occurs when dalamud configuration is saved. /// - public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved; + public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved; /// /// Gets or sets a list of muted works. /// - public List BadWords { get; set; } + public List? BadWords { get; set; } /// /// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found. @@ -68,12 +69,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the language code to load Dalamud localization with. /// - public string LanguageOverride { get; set; } = null; + public string? LanguageOverride { get; set; } = null; /// /// Gets or sets the last loaded Dalamud version. /// - public string LastVersion { get; set; } = null; + public string? LastVersion { get; set; } = null; /// /// Gets or sets a value indicating the last seen FTUE version. @@ -84,7 +85,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the last loaded Dalamud version. /// - public string LastChangelogMajorMinor { get; set; } = null; + public string? LastChangelogMajorMinor { get; set; } = null; /// /// Gets or sets the chat type used by default for plugin messages. @@ -229,6 +230,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// This setting is effected by the in-game "System Sounds" option and volume. /// + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")] public bool EnablePluginUISoundEffects { get; set; } /// @@ -266,7 +268,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the kind of beta to download when matches the server value. /// - public string DalamudBetaKind { get; set; } + public string? DalamudBetaKind { get; set; } /// /// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started. @@ -514,6 +516,8 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable private void Save() { ThreadSafety.AssertMainThread(); + if (this.configPath is null) + throw new InvalidOperationException("configPath is not set."); Service.Get().WriteAllText( this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); diff --git a/Dalamud/Interface/GameFonts/FdtReader.cs b/Dalamud/Interface/GameFonts/FdtReader.cs index a68caba94..0e8f3fb59 100644 --- a/Dalamud/Interface/GameFonts/FdtReader.cs +++ b/Dalamud/Interface/GameFonts/FdtReader.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -22,7 +21,7 @@ public class FdtReader for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++) this.Glyphs.Add(StructureFromByteArray(data, this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf() + (Marshal.SizeOf() * i))); - for (int i = 0, i_ = Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); i < i_; i++) + for (int i = 0, to = Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); i < to; i++) this.Distances.Add(StructureFromByteArray(data, this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf() + (Marshal.SizeOf() * i))); } @@ -51,6 +50,14 @@ public class FdtReader /// public List Distances { get; init; } = new(); + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) => + this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) }); + /// /// Finds glyph definition for corresponding codepoint. /// @@ -58,7 +65,7 @@ public class FdtReader /// Corresponding FontTableEntry, or null if not found. public FontTableEntry? FindGlyph(int codepoint) { - var i = this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) }); + var i = this.FindGlyphIndex(codepoint); if (i < 0 || i == this.Glyphs.Count) return null; return this.Glyphs[i]; @@ -91,17 +98,12 @@ public class FdtReader return this.Distances[i].RightOffset; } - private static unsafe T StructureFromByteArray(byte[] data, int offset) - { - var len = Marshal.SizeOf(); - if (offset + len > data.Length) - throw new Exception("Data too short"); - - fixed (byte* ptr = data) - return Marshal.PtrToStructure(new(ptr + offset)); - } - - private static int CodePointToUtf8Int32(int codepoint) + /// + /// Translates a UTF-32 codepoint to a containing a UTF-8 character. + /// + /// The codepoint. + /// The uint. + internal static int CodePointToUtf8Int32(int codepoint) { if (codepoint <= 0x7F) { @@ -131,6 +133,16 @@ public class FdtReader } } + private static unsafe T StructureFromByteArray(byte[] data, int offset) + { + var len = Marshal.SizeOf(); + if (offset + len > data.Length) + throw new Exception("Data too short"); + + fixed (byte* ptr = data) + return Marshal.PtrToStructure(new(ptr + offset)); + } + private static int Utf8Uint32ToCodePoint(int n) { if ((n & 0xFFFFFF80) == 0) @@ -252,7 +264,7 @@ public class FdtReader /// Glyph table entry. /// [StructLayout(LayoutKind.Sequential)] - public unsafe struct FontTableEntry : IComparable + public struct FontTableEntry : IComparable { /// /// Mapping of texture channel index to byte index. @@ -367,7 +379,7 @@ public class FdtReader /// Kerning table entry. /// [StructLayout(LayoutKind.Sequential)] - public unsafe struct KerningTableEntry : IComparable + public struct KerningTableEntry : IComparable { /// /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character. diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index a7cd27b83..71661682d 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -257,7 +257,7 @@ internal class GameFontManager : IServiceType /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - ImGuiHelpers.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -269,7 +269,7 @@ internal class GameFontManager : IServiceType /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); } /// diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 40b810161..946473df4 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -1,5 +1,3 @@ -using System; - namespace Dalamud.Interface.GameFonts; /// @@ -153,7 +151,7 @@ public struct GameFontStyle GameFontFamilyAndSize.TrumpGothic184 => 18.4f, GameFontFamilyAndSize.TrumpGothic23 => 23, GameFontFamilyAndSize.TrumpGothic34 => 34, - GameFontFamilyAndSize.TrumpGothic68 => 8, + GameFontFamilyAndSize.TrumpGothic68 => 68, _ => throw new InvalidOperationException(), }; @@ -186,77 +184,77 @@ public struct GameFontStyle /// Font family. /// Font size in points. /// Recommended GameFontFamilyAndSize. - public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) - { - if (size <= 0) - return GameFontFamilyAndSize.Undefined; - - switch (family) + public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) => + family switch { - case GameFontFamily.Undefined: - return GameFontFamilyAndSize.Undefined; + _ when size <= 0 => GameFontFamilyAndSize.Undefined, + GameFontFamily.Undefined => GameFontFamilyAndSize.Undefined, + GameFontFamily.Axis => size switch + { + <= ((int)((9.6f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis96, + <= ((int)((12f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis12, + <= ((int)((14f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis14, + <= ((int)((18f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis18, + _ => GameFontFamilyAndSize.Axis36, + }, + GameFontFamily.Jupiter => size switch + { + <= ((int)((16f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter16, + <= ((int)((20f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter20, + <= ((int)((23f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter23, + _ => GameFontFamilyAndSize.Jupiter46, + }, + GameFontFamily.JupiterNumeric => size switch + { + <= ((int)((45f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter45, + _ => GameFontFamilyAndSize.Jupiter90, + }, + GameFontFamily.Meidinger => size switch + { + <= ((int)((16f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Meidinger16, + <= ((int)((20f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Meidinger20, + _ => GameFontFamilyAndSize.Meidinger40, + }, + GameFontFamily.MiedingerMid => size switch + { + <= ((int)((10f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid10, + <= ((int)((12f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid12, + <= ((int)((14f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid14, + <= ((int)((18f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid18, + _ => GameFontFamilyAndSize.MiedingerMid36, + }, + GameFontFamily.TrumpGothic => size switch + { + <= ((int)((18.4f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic184, + <= ((int)((23f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic23, + <= ((int)((34f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic34, + _ => GameFontFamilyAndSize.TrumpGothic68, + }, + _ => GameFontFamilyAndSize.Undefined, + }; - case GameFontFamily.Axis: - if (size <= 9.601) - return GameFontFamilyAndSize.Axis96; - else if (size <= 12.001) - return GameFontFamilyAndSize.Axis12; - else if (size <= 14.001) - return GameFontFamilyAndSize.Axis14; - else if (size <= 18.001) - return GameFontFamilyAndSize.Axis18; - else - return GameFontFamilyAndSize.Axis36; - - case GameFontFamily.Jupiter: - if (size <= 16.001) - return GameFontFamilyAndSize.Jupiter16; - else if (size <= 20.001) - return GameFontFamilyAndSize.Jupiter20; - else if (size <= 23.001) - return GameFontFamilyAndSize.Jupiter23; - else - return GameFontFamilyAndSize.Jupiter46; - - case GameFontFamily.JupiterNumeric: - if (size <= 45.001) - return GameFontFamilyAndSize.Jupiter45; - else - return GameFontFamilyAndSize.Jupiter90; - - case GameFontFamily.Meidinger: - if (size <= 16.001) - return GameFontFamilyAndSize.Meidinger16; - else if (size <= 20.001) - return GameFontFamilyAndSize.Meidinger20; - else - return GameFontFamilyAndSize.Meidinger40; - - case GameFontFamily.MiedingerMid: - if (size <= 10.001) - return GameFontFamilyAndSize.MiedingerMid10; - else if (size <= 12.001) - return GameFontFamilyAndSize.MiedingerMid12; - else if (size <= 14.001) - return GameFontFamilyAndSize.MiedingerMid14; - else if (size <= 18.001) - return GameFontFamilyAndSize.MiedingerMid18; - else - return GameFontFamilyAndSize.MiedingerMid36; - - case GameFontFamily.TrumpGothic: - if (size <= 18.401) - return GameFontFamilyAndSize.TrumpGothic184; - else if (size <= 23.001) - return GameFontFamilyAndSize.TrumpGothic23; - else if (size <= 34.001) - return GameFontFamilyAndSize.TrumpGothic34; - else - return GameFontFamilyAndSize.TrumpGothic68; - - default: - return GameFontFamilyAndSize.Undefined; + /// + /// Calculates the adjustment to width resulting fron Weight and SkewStrength. + /// + /// Font header. + /// Glyph. + /// Width adjustment in pixel unit. + public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + { + var widthDelta = this.Weight; + switch (this.BaseSkewStrength) + { + case > 0: + widthDelta += (1f * this.BaseSkewStrength * (header.LineHeight - glyph.CurrentOffsetY)) + / header.LineHeight; + break; + case < 0: + widthDelta -= (1f * this.BaseSkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight)) + / header.LineHeight; + break; } + + return (int)MathF.Ceiling(widthDelta); } /// @@ -265,16 +263,8 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) - { - var widthDelta = this.Weight; - if (this.BaseSkewStrength > 0) - widthDelta += 1f * this.BaseSkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight; - else if (this.BaseSkewStrength < 0) - widthDelta -= 1f * this.BaseSkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight; - - return (int)Math.Ceiling(widthDelta); - } + public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// public override string ToString() diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d5394fe8d..9de87c6e3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -52,8 +52,16 @@ namespace Dalamud.Interface.Internal; [ServiceManager.BlockingEarlyLoadedService] internal class InterfaceManager : IDisposable, IServiceType { - private const float DefaultFontSizePt = 12.0f; - private const float DefaultFontSizePx = DefaultFontSizePt * 4.0f / 3.0f; + /// + /// The default font size, in points. + /// + public const float DefaultFontSizePt = 12.0f; + + /// + /// The default font size, in pixels. + /// + public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; + private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index d59b50e58..e9d4152a5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -18,39 +18,39 @@ internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { - new ServicesWidget(), - new AddressesWidget(), - new ObjectTableWidget(), - new FateTableWidget(), - new SeFontTestWidget(), - new FontAwesomeTestWidget(), - new PartyListWidget(), - new BuddyListWidget(), - new PluginIpcWidget(), - new ConditionWidget(), - new GaugeWidget(), - new CommandWidget(), - new AddonWidget(), new AddonInspectorWidget(), + new AddonLifecycleWidget(), + new AddonWidget(), + new AddressesWidget(), + new AetherytesWidget(), new AtkArrayDataBrowserWidget(), + new BuddyListWidget(), + new CommandWidget(), + new ConditionWidget(), + new ConfigurationWidget(), + new DataShareWidget(), + new DtrBarWidget(), + new FateTableWidget(), + new FlyTextWidget(), + new FontAwesomeTestWidget(), + new GamepadWidget(), + new GaugeWidget(), + new HookWidget(), + new IconBrowserWidget(), + new ImGuiWidget(), + new KeyStateWidget(), + new NetworkMonitorWidget(), + new ObjectTableWidget(), + new PartyListWidget(), + new PluginIpcWidget(), + new SeFontTestWidget(), + new ServicesWidget(), new StartInfoWidget(), new TargetWidget(), - new ToastWidget(), - new FlyTextWidget(), - new ImGuiWidget(), - new TexWidget(), - new KeyStateWidget(), - new GamepadWidget(), - new ConfigurationWidget(), new TaskSchedulerWidget(), - new HookWidget(), - new AetherytesWidget(), - new DtrBarWidget(), + new TexWidget(), + new ToastWidget(), new UIColorWidget(), - new DataShareWidget(), - new NetworkMonitorWidget(), - new IconBrowserWidget(), - new AddonLifecycleWidget(), }; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs index 0e12e4c51..78df015ed 100644 --- a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs @@ -1,7 +1,6 @@ -using System; -using System.Linq; +using System.Linq; -namespace Dalamud.Interface.Internal.Windows; +namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class representing a date window entry. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 7dc8e2f3c..0e654d316 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -28,8 +28,14 @@ public class AddonLifecycleWidget : IDataWindowWidget /// public void Load() { - this.AddonLifecycle = Service.GetNullable(); - if (this.AddonLifecycle is not null) this.Ready = true; + Service + .GetAsync() + .ContinueWith( + r => + { + this.AddonLifecycle = r.Result; + this.Ready = true; + }); } /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 26bd2e623..22f615e8a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -38,12 +38,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget public void Draw() { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - this.iconCategories ??= FontAwesomeHelpers.GetCategories(); + + this.iconCategories ??= new[] { "(Show All)", "(Undefined)" } + .Concat(FontAwesomeHelpers.GetCategories().Skip(1)) + .ToArray(); if (this.iconSearchChanged) { - this.icons = FontAwesomeHelpers.SearchIcons(this.iconSearchInput, this.iconCategories[this.selectedIconCategory]); + if (this.iconSearchInput == string.Empty && this.selectedIconCategory <= 1) + { + var en = InterfaceManager.IconFont.GlyphsWrapped() + .Select(x => (FontAwesomeIcon)x.Codepoint) + .Where(x => (ushort)x is >= 0xE000 and < 0xF000); + en = this.selectedIconCategory == 0 + ? en.Concat(FontAwesomeHelpers.SearchIcons(string.Empty, string.Empty)) + : en.Except(FontAwesomeHelpers.SearchIcons(string.Empty, string.Empty)); + this.icons = en.Distinct().Order().ToList(); + } + else + { + this.icons = FontAwesomeHelpers.SearchIcons( + this.iconSearchInput, + this.selectedIconCategory <= 1 ? string.Empty : this.iconCategories[this.selectedIconCategory]); + } + this.iconNames = this.icons.Select(icon => Enum.GetName(icon)!).ToList(); this.iconSearchChanged = false; } diff --git a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs index 16f253da9..cd653143b 100644 --- a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -46,7 +45,7 @@ public class ProfilerWindow : Window ImGui.Text("Timings"); - var childHeight = Math.Max(300, 20 * (2 + this.occupied.Count)); + var childHeight = Math.Max(300, 20 * (2.5f + this.occupied.Count)); if (ImGui.BeginChild("Timings", new Vector2(0, childHeight), true)) { @@ -115,7 +114,7 @@ public class ProfilerWindow : Window parentDepthDict[timingHandle.Id] = depth; startX = Math.Max(startX, 0); - endX = Math.Max(endX, 0); + endX = Math.Max(endX, startX + (ImGuiHelpers.GlobalScale * 16)); Vector4 rectColor; if (this.occupied[depth].Count % 2 == 0) @@ -129,11 +128,6 @@ public class ProfilerWindow : Window if (maxRectDept < depth) maxRectDept = (uint)depth; - if (startX == endX) - { - continue; - } - var minPos = pos + new Vector2((uint)startX, 20 * depth); var maxPos = pos + new Vector2((uint)endX, 20 * (depth + 1)); @@ -231,22 +225,22 @@ public class ProfilerWindow : Window ImGui.EndChild(); var sliderMin = (float)this.min / 1000f; - if (ImGui.SliderFloat("Start", ref sliderMin, (float)actualMin / 1000f, (float)this.max / 1000f, "%.1fs")) + if (ImGui.SliderFloat("Start", ref sliderMin, (float)actualMin / 1000f, (float)this.max / 1000f, "%.2fs")) { this.min = sliderMin * 1000f; } var sliderMax = (float)this.max / 1000f; - if (ImGui.SliderFloat("End", ref sliderMax, (float)this.min / 1000f, (float)actualMax / 1000f, "%.1fs")) + if (ImGui.SliderFloat("End", ref sliderMax, (float)this.min / 1000f, (float)actualMax / 1000f, "%.2fs")) { this.max = sliderMax * 1000f; } - var sizeShown = (float)(this.max - this.min); - var sizeActual = (float)(actualMax - actualMin); - if (ImGui.SliderFloat("Size", ref sizeShown, sizeActual / 10f, sizeActual, "%.1fs")) + var sizeShown = (float)(this.max - this.min) / 1000f; + var sizeActual = (float)(actualMax - actualMin) / 1000f; + if (ImGui.SliderFloat("Size", ref sizeShown, sizeActual / 10f, sizeActual, "%.2fs")) { - this.max = this.min + sizeShown; + this.max = this.min + (sizeShown * 1000f); } ImGui.Text("Min: " + actualMin.ToString("0.000")); @@ -257,6 +251,7 @@ public class ProfilerWindow : Window [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internals")] private class RectInfo { + // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized <- well you're wrong internal TimingHandle Timing; internal Vector2 MinPos; internal Vector2 MaxPos; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 7a6f894c1..02e8ce789 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,5 +1,6 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; using CheapLoc; using Dalamud.Configuration.Internal; @@ -16,6 +17,16 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] public class SettingsTabLook : SettingsTab { + private static readonly (string, float)[] GlobalUiScalePresets = + { + ("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt), + ("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt), + ("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt), + ("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt), + ("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt), + ("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt), + }; + private float globalUiScale; private float fontGamma; @@ -135,55 +146,22 @@ public class SettingsTabLook : SettingsTab { var interfaceManager = Service.Get(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); - ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3); - if (ImGui.Button("9.6pt##DalamudSettingsGlobalUiScaleReset96")) - { - this.globalUiScale = 9.6f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - ImGui.SameLine(); - if (ImGui.Button("12pt##DalamudSettingsGlobalUiScaleReset12")) + var buttonSize = + GlobalUiScalePresets + .Select(x => ImGui.CalcTextSize(x.Item1, 0, x.Item1.IndexOf('#'))) + .Aggregate(Vector2.Zero, Vector2.Max) + + (ImGui.GetStyle().FramePadding * 2); + foreach (var (buttonLabel, scale) in GlobalUiScalePresets) { - this.globalUiScale = 1.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("14pt##DalamudSettingsGlobalUiScaleReset14")) - { - this.globalUiScale = 14.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("18pt##DalamudSettingsGlobalUiScaleReset18")) - { - this.globalUiScale = 18.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("24pt##DalamudSettingsGlobalUiScaleReset24")) - { - this.globalUiScale = 24.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("36pt##DalamudSettingsGlobalUiScaleReset36")) - { - this.globalUiScale = 36.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); + ImGui.SameLine(); + if (ImGui.Button(buttonLabel, buttonSize) && Math.Abs(this.globalUiScale - scale) > float.Epsilon) + { + ImGui.GetIO().FontGlobalScale = this.globalUiScale = scale; + interfaceManager.RebuildFonts(); + } } var globalUiScaleInPt = 12f * this.globalUiScale; @@ -198,10 +176,9 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.ScaledDummy(5); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3); if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) { this.fontGamma = 1.4f; diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index d11e1ce1c..579d93f86 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Utility.Raii; using ImGuiNET; @@ -25,6 +26,20 @@ public static class ImGuiHelpers /// public static float GlobalScale { get; private set; } + /// + /// Gets a value indicating whether ImGui is initialized and ready for use.
+ /// This does not necessarily mean you can call drawing functions. + ///
+ public static unsafe bool IsImGuiInitialized => + ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null; + + /// + /// Gets the global Dalamud scale; even available before drawing is ready.
+ /// If you are sure that drawing is ready, at the point of using this, use instead. + ///
+ public static float GlobalScaleSafe => + IsImGuiInitialized ? ImGui.GetIO().FontGlobalScale : Service.Get().GlobalUiScale; + /// /// Check if the current ImGui window is on the main viewport. /// Only valid within a window. @@ -174,6 +189,47 @@ public static class ImGuiHelpers } } + /// + /// Unscales fonts after they have been rendered onto atlas. + /// + /// Font to scale. + /// Scale. + /// If a positive number is given, numbers will be rounded to this. + public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) + { + Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + + var font = fontPtr.NativePtr; + font->FontSize = rounder(font->FontSize * scale); + font->Ascent = rounder(font->Ascent * scale); + font->Descent = font->FontSize - font->Ascent; + if (font->ConfigData != null) + font->ConfigData->SizePixels = rounder(font->ConfigData->SizePixels * scale); + + foreach (ref var glyphHotDataReal in new Span( + (void*)font->IndexedHotData.Data, + font->IndexedHotData.Size)) + { + glyphHotDataReal.AdvanceX = rounder(glyphHotDataReal.AdvanceX * scale); + glyphHotDataReal.OccupiedWidth = rounder(glyphHotDataReal.OccupiedWidth * scale); + } + + foreach (ref var glyphReal in new Span((void*)font->Glyphs.Data, font->Glyphs.Size)) + { + glyphReal.X0 *= scale; + glyphReal.X1 *= scale; + glyphReal.Y0 *= scale; + glyphReal.Y1 *= scale; + glyphReal.AdvanceX = rounder(glyphReal.AdvanceX * scale); + } + + foreach (ref var kp in new Span((void*)font->KerningPairs.Data, font->KerningPairs.Size)) + kp.AdvanceXAdjustment = rounder(kp.AdvanceXAdjustment * scale); + + foreach (ref var fkp in new Span((void*)font->FrequentKerningPairs.Data, font->FrequentKerningPairs.Size)) + fkp = rounder(fkp * scale); + } + /// /// Fills missing glyphs in target font from source font, if both are not null. /// @@ -183,71 +239,110 @@ public static class ImGuiHelpers /// Whether to call target.BuildLookupTable(). /// Low codepoint range to copy. /// High codepoing range to copy. - public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) + [Obsolete("Use the non-nullable variant.", true)] + public static void CopyGlyphsAcrossFonts( + ImFontPtr? source, + ImFontPtr? target, + bool missingOnly, + bool rebuildLookupTable = true, + int rangeLow = 32, + int rangeHigh = 0xFFFE) => + CopyGlyphsAcrossFonts( + source ?? default, + target ?? default, + missingOnly, + rebuildLookupTable, + rangeLow, + rangeHigh); + + /// + /// 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(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + public static unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + int rangeLow = 32, + int rangeHigh = 0xFFFE) { - if (!source.HasValue || !target.HasValue) + if (!source.IsNotNullAndLoaded() || !target.IsNotNullAndLoaded()) return; - var scale = target.Value!.FontSize / source.Value!.FontSize; + var changed = false; + var scale = target.FontSize / source.FontSize; var addedCodepoints = new HashSet(); - unsafe + + if (source.Glyphs.Size == 0) + return; + + var glyphs = (ImFontGlyphReal*)source.Glyphs.Data; + if (glyphs is null) + throw new InvalidOperationException("Glyphs data is empty but size is >0?"); + + for (int j = 0, k = source.Glyphs.Size; j < k; j++) { - var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; - for (int j = 0, k = source.Value!.Glyphs.Size; j < k; j++) + var glyph = &glyphs![j]; + if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) + continue; + + var prevGlyphPtr = (ImFontGlyphReal*)target.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; + if ((IntPtr)prevGlyphPtr == IntPtr.Zero) { - Debug.Assert(glyphs != null, nameof(glyphs) + " != null"); - - var glyph = &glyphs[j]; - if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) - continue; - - var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; - if ((IntPtr)prevGlyphPtr == IntPtr.Zero) - { - addedCodepoints.Add(glyph->Codepoint); - target.Value!.AddGlyph( - target.Value!.ConfigData, - (ushort)glyph->Codepoint, - glyph->TextureIndex, - glyph->X0 * scale, - ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->X1 * scale, - ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->U0, - glyph->V0, - glyph->U1, - glyph->V1, - glyph->AdvanceX * scale); - } - else if (!missingOnly) - { - addedCodepoints.Add(glyph->Codepoint); - prevGlyphPtr->TextureIndex = glyph->TextureIndex; - prevGlyphPtr->X0 = glyph->X0 * scale; - prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->X1 = glyph->X1 * scale; - prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->U0 = glyph->U0; - prevGlyphPtr->V0 = glyph->V0; - prevGlyphPtr->U1 = glyph->U1; - prevGlyphPtr->V1 = glyph->V1; - prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; - } + addedCodepoints.Add(glyph->Codepoint); + target.AddGlyph( + target.ConfigData, + (ushort)glyph->Codepoint, + glyph->TextureIndex, + glyph->X0 * scale, + ((glyph->Y0 - source.Ascent) * scale) + target.Ascent, + glyph->X1 * scale, + ((glyph->Y1 - source.Ascent) * scale) + target.Ascent, + glyph->U0, + glyph->V0, + glyph->U1, + glyph->V1, + glyph->AdvanceX * scale); + changed = true; } - - var kernPairs = source.Value!.KerningPairs; - for (int j = 0, k = kernPairs.Size; j < k; j++) + else if (!missingOnly) { - if (!addedCodepoints.Contains(kernPairs[j].Left)) - continue; - if (!addedCodepoints.Contains(kernPairs[j].Right)) - continue; - target.Value.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment); + addedCodepoints.Add(glyph->Codepoint); + prevGlyphPtr->TextureIndex = glyph->TextureIndex; + prevGlyphPtr->X0 = glyph->X0 * scale; + prevGlyphPtr->Y0 = ((glyph->Y0 - source.Ascent) * scale) + target.Ascent; + prevGlyphPtr->X1 = glyph->X1 * scale; + prevGlyphPtr->Y1 = ((glyph->Y1 - source.Ascent) * scale) + target.Ascent; + prevGlyphPtr->U0 = glyph->U0; + prevGlyphPtr->V0 = glyph->V0; + prevGlyphPtr->U1 = glyph->U1; + prevGlyphPtr->V1 = glyph->V1; + prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; } } - if (rebuildLookupTable && target.Value!.Glyphs.Size > 0) - target.Value!.BuildLookupTableNonstandard(); + if (target.Glyphs.Size == 0) + return; + + var kernPairs = source.KerningPairs; + for (int j = 0, k = kernPairs.Size; j < k; j++) + { + if (!addedCodepoints.Contains(kernPairs[j].Left)) + continue; + if (!addedCodepoints.Contains(kernPairs[j].Right)) + continue; + target.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment); + changed = true; + } + + if (changed && rebuildLookupTable) + target.BuildLookupTableNonstandard(); } /// @@ -302,21 +397,35 @@ public static class ImGuiHelpers /// Center the ImGui cursor for a certain text. /// /// The text to center for. - public static void CenterCursorForText(string text) - { - var textWidth = ImGui.CalcTextSize(text).X; - CenterCursorFor((int)textWidth); - } + public static void CenterCursorForText(string text) => CenterCursorFor(ImGui.CalcTextSize(text).X); /// /// Center the ImGui cursor for an item with a certain width. /// /// The width to center for. - public static void CenterCursorFor(int itemWidth) - { - var window = (int)ImGui.GetWindowWidth(); - ImGui.SetCursorPosX((window / 2) - (itemWidth / 2)); - } + public static void CenterCursorFor(float itemWidth) => + ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + + /// + /// Determines whether is empty. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; + + /// + /// Determines whether is not null and loaded. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNotNullAndLoaded(this ImFontPtr ptr) => ptr.NativePtr != null && ptr.IsLoaded(); + + /// + /// Determines whether is empty. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; /// /// Get data needed for each new frame. @@ -330,19 +439,57 @@ public static class ImGuiHelpers /// ImFontGlyph the correct version. /// [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")] + [StructLayout(LayoutKind.Explicit, Size = 40)] public struct ImFontGlyphReal { + [FieldOffset(0)] public uint ColoredVisibleTextureIndexCodepoint; + + [FieldOffset(4)] public float AdvanceX; + + [FieldOffset(8)] public float X0; + + [FieldOffset(12)] public float Y0; + + [FieldOffset(16)] public float X1; + + [FieldOffset(20)] public float Y1; + + [FieldOffset(24)] public float U0; + + [FieldOffset(28)] public float V0; + + [FieldOffset(32)] public float U1; + + [FieldOffset(36)] public float V1; + [FieldOffset(8)] + public Vector2 XY0; + + [FieldOffset(16)] + public Vector2 XY1; + + [FieldOffset(24)] + public Vector2 UV0; + + [FieldOffset(32)] + public Vector2 UV1; + + [FieldOffset(8)] + public Vector4 XY; + + [FieldOffset(24)] + public Vector4 UV; + private const uint ColoredMask /*****/ = 0b_00000000_00000000_00000000_00000001u; private const uint VisibleMask /*****/ = 0b_00000000_00000000_00000000_00000010u; private const uint TextureMask /*****/ = 0b_00000000_00000000_00000111_11111100u; @@ -390,7 +537,7 @@ public static class ImGuiHelpers private const uint UseBisectMask /***/ = 0b_00000000_00000000_00000000_00000001u; private const uint OffsetMask /******/ = 0b_00000000_00001111_11111111_11111110u; - private const uint CountMask /*******/ = 0b_11111111_11110000_00000111_11111100u; + private const uint CountMask /*******/ = 0b_11111111_11110000_00000000_00000000u; private const int UseBisectShift = 0; private const int OffsetShift = 1; @@ -419,6 +566,7 @@ public static class ImGuiHelpers /// ImFontAtlasCustomRect the correct version. /// [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")] + [StructLayout(LayoutKind.Sequential)] public unsafe struct ImFontAtlasCustomRectReal { public ushort Width; @@ -431,10 +579,10 @@ public static class ImGuiHelpers public ImFont* Font; private const uint TextureIndexMask /***/ = 0b_00000000_00000000_00000111_11111100u; - private const uint GlyphIDMask /********/ = 0b_11111111_11111111_11111000_00000000u; + private const uint GlyphIdMask /********/ = 0b_11111111_11111111_11111000_00000000u; private const int TextureIndexShift = 2; - private const int GlyphIDShift = 11; + private const int GlyphIdShift = 11; public int TextureIndex { @@ -444,8 +592,8 @@ public static class ImGuiHelpers public int GlyphId { - get => (int)(this.TextureIndexAndGlyphId & GlyphIDMask) >> GlyphIDShift; - set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIDMask) | ((uint)value << GlyphIDShift); + get => (int)(this.TextureIndexAndGlyphId & GlyphIdMask) >> GlyphIdShift; + set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIdMask) | ((uint)value << GlyphIdShift); } } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs new file mode 100644 index 000000000..67b002179 --- /dev/null +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -0,0 +1,687 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.Utility; + +/// +/// Utility methods for . +/// +public static class ImVectorWrapper +{ + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromEnumerable( + IEnumerable sourceEnumerable, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(0, destroyer); + try + { + switch (sourceEnumerable) + { + case T[] c: + res.SetCapacity(Math.Max(minCapacity, c.Length + 1)); + res.LengthUnsafe = c.Length; + c.AsSpan().CopyTo(res.DataSpan); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + default: + res.SetCapacity(minCapacity); + res.AddRange(sourceEnumerable); + res.EnsureCapacity(res.LengthUnsafe + 1); + break; + } + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromSpan( + ReadOnlySpan sourceSpan, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(Math.Max(minCapacity, sourceSpan.Length + 1), destroyer); + try + { + res.LengthUnsafe = sourceSpan.Length; + sourceSpan.CopyTo(res.DataSpan); + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper ConfigDataWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->ConfigData, ImGuiNative.ImFontConfig_destroy); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper FontsWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Fonts, x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper TexturesWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Textures); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper GlyphsWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Glyphs); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexedHotDataWrapped(this ImFontPtr obj) + => obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexedHotData); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexLookupWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexLookup); +} + +/// +/// Wrapper for ImVector. +/// +/// Contained type. +public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDisposable + where T : unmanaged +{ + private ImVector* vector; + private ImGuiNativeDestroyDelegate? destroyer; + + /// + /// Initializes a new instance of the struct.
+ /// If is set to true, you must call after use, + /// and the underlying memory for must have been allocated using + /// . Otherwise, it will crash. + ///
+ /// The underlying vector. + /// The destroyer function to call on item removal. + /// Whether this wrapper owns the vector. + public ImVectorWrapper( + [NotNull] ImVector* vector, + ImGuiNativeDestroyDelegate? destroyer = null, + bool ownership = false) + { + if (vector is null) + throw new ArgumentException($"{nameof(vector)} cannot be null.", nameof(this.vector)); + + this.vector = vector; + this.destroyer = destroyer; + this.HasOwnership = ownership; + } + + /// + /// Initializes a new instance of the struct.
+ /// You must call after use. + ///
+ /// The initial capacity. + /// The destroyer function to call on item removal. + public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null) + { + if (initialCapacity < 0) + { + throw new ArgumentOutOfRangeException( + nameof(initialCapacity), + initialCapacity, + $"{nameof(initialCapacity)} cannot be a negative number."); + } + + this.vector = (ImVector*)ImGuiNative.igMemAlloc((uint)sizeof(ImVector)); + if (this.vector is null) + throw new OutOfMemoryException(); + *this.vector = default; + this.HasOwnership = true; + this.destroyer = destroyer; + + try + { + this.EnsureCapacity(initialCapacity); + } + catch + { + ImGuiNative.igMemFree(this.vector); + this.vector = null; + this.HasOwnership = false; + this.destroyer = null; + throw; + } + } + + /// + /// Destroy callback for items. + /// + /// Pointer to self. + public delegate void ImGuiNativeDestroyDelegate(T* self); + + /// + /// Gets the raw vector. + /// + public ImVector* RawVector => this.vector; + + /// + /// Gets a view of the underlying ImVector{T}, for the range of . + /// + public Span DataSpan => new(this.DataUnsafe, this.LengthUnsafe); + + /// + /// Gets a view of the underlying ImVector{T}, for the range of . + /// + public Span StorageSpan => new(this.DataUnsafe, this.CapacityUnsafe); + + /// + /// Gets a value indicating whether this is disposed. + /// + public bool IsDisposed => this.vector is null; + + /// + /// Gets a value indicating whether this has the ownership of the underlying + /// . + /// + public bool HasOwnership { get; private set; } + + /// + /// Gets the underlying . + /// + public ImVector* Vector => + this.vector is null ? throw new ObjectDisposedException(nameof(ImVectorWrapper)) : this.vector; + + /// + /// Gets the number of items contained inside the underlying ImVector{T}. + /// + public int Length => this.LengthUnsafe; + + /// + /// Gets the number of items that can be contained inside the underlying ImVector{T}. + /// + public int Capacity => this.CapacityUnsafe; + + /// + /// Gets the pointer to the first item in the data inside underlying ImVector{T}. + /// + public T* Data => this.DataUnsafe; + + /// + /// Gets the reference to the number of items contained inside the underlying ImVector{T}. + /// + public ref int LengthUnsafe => ref *&this.Vector->Size; + + /// + /// Gets the reference to the number of items that can be contained inside the underlying ImVector{T}. + /// + public ref int CapacityUnsafe => ref *&this.Vector->Capacity; + + /// + /// Gets the reference to the pointer to the first item in the data inside underlying ImVector{T}. + /// + /// This may be null, if is zero. + public ref T* DataUnsafe => ref *(T**)&this.Vector->Data; + + /// + public bool IsReadOnly => false; + + /// + int ICollection.Count => this.LengthUnsafe; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot { get; } = new(); + + /// + int ICollection.Count => this.LengthUnsafe; + + /// + int IReadOnlyCollection.Count => this.LengthUnsafe; + + /// + bool IList.IsFixedSize => false; + + /// + /// Gets the element at the specified index as a reference. + /// + /// Index of the item. + /// If is out of range. + public ref T this[int index] => ref this.DataUnsafe[this.EnsureIndex(index)]; + + /// + T IReadOnlyList.this[int index] => this[index]; + + /// + object? IList.this[int index] + { + get => this[index]; + set => this[index] = value is null ? default : (T)value; + } + + /// + T IList.this[int index] + { + get => this[index]; + set => this[index] = value; + } + + /// + public void Dispose() + { + if (this.HasOwnership) + { + this.Clear(); + this.SetCapacity(0); + Debug.Assert(this.vector->Data == 0, "SetCapacity(0) did not free the data"); + ImGuiNative.igMemFree(this.vector); + } + + this.vector = null; + this.HasOwnership = false; + this.destroyer = null; + } + + /// + public IEnumerator GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + yield return this[i]; + } + + /// + public void Add(in T item) + { + this.EnsureCapacityExponential(this.LengthUnsafe + 1); + this.DataUnsafe[this.LengthUnsafe++] = item; + } + + /// + public void AddRange(IEnumerable items) + { + if (items is ICollection { Count: var count }) + this.EnsureCapacityExponential(this.LengthUnsafe + count); + + foreach (var item in items) + this.Add(item); + } + + /// + public void AddRange(Span items) + { + this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); + foreach (var item in items) + this.Add(item); + } + + /// + public void Clear() => this.Clear(false); + + /// + /// Clears this vector, optionally skipping destroyer invocation. + /// + /// Whether to skip destroyer invocation. + public void Clear(bool skipDestroyer) + { + if (this.destroyer != null && !skipDestroyer) + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + this.destroyer(&this.DataUnsafe[i]); + } + + this.LengthUnsafe = 0; + } + + /// + public bool Contains(in T item) => this.IndexOf(in item) != -1; + + /// + /// Size down the underlying ImVector{T}. + /// + /// Capacity to reserve. + /// Whether the capacity has been changed. + public bool Compact(int reservation) => this.SetCapacity(Math.Max(reservation, this.LengthUnsafe)); + + /// + public void CopyTo(T[] array, int arrayIndex) + { + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException( + nameof(arrayIndex), + arrayIndex, + $"{nameof(arrayIndex)} is less than 0."); + } + + if (array.Length - arrayIndex < this.LengthUnsafe) + { + throw new ArgumentException( + "The number of elements in the source ImVectorWrapper is greater than the available space from arrayIndex to the end of the destination array.", + nameof(array)); + } + + fixed (void* p = array) + Buffer.MemoryCopy(this.DataUnsafe, p, this.LengthUnsafe * sizeof(T), this.LengthUnsafe * sizeof(T)); + } + + /// + /// Ensures that the capacity of this list is at least the specified .
+ /// On growth, the new capacity exactly matches . + ///
+ /// The minimum capacity to ensure. + /// Whether the capacity has been changed. + public bool EnsureCapacity(int capacity) => this.CapacityUnsafe < capacity && this.SetCapacity(capacity); + + /// + /// Ensures that the capacity of this list is at least the specified .
+ /// On growth, the new capacity may exceed . + ///
+ /// The minimum capacity to ensure. + /// Whether the capacity has been changed. + public bool EnsureCapacityExponential(int capacity) + => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe))); + + /// + /// Resizes the underlying array and fills with zeroes if grown. + /// + /// New size. + /// New default value. + /// Whether to skip calling destroyer function. + public void Resize(int size, in T defaultValue = default, bool skipDestroyer = false) + { + this.EnsureCapacity(size); + var old = this.LengthUnsafe; + if (old > size && !skipDestroyer && this.destroyer is not null) + { + foreach (var v in this.DataSpan[size..]) + this.destroyer(&v); + } + + this.LengthUnsafe = size; + if (old < size) + this.DataSpan[old..].Fill(defaultValue); + } + + /// + public bool Remove(in T item) + { + var index = this.IndexOf(item); + if (index == -1) + return false; + + this.RemoveAt(index); + return true; + } + + /// + public int IndexOf(in T item) + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + { + if (Equals(item, this.DataUnsafe[i])) + return i; + } + + return -1; + } + + /// + public void Insert(int index, in T item) + { + // Note: index == this.LengthUnsafe is okay; we're just adding to the end then + if (index < 0 || index > this.LengthUnsafe) + throw new IndexOutOfRangeException(); + + this.EnsureCapacityExponential(this.CapacityUnsafe + 1); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T)); + this.DataUnsafe[index] = item; + } + + /// + public void InsertRange(int index, IEnumerable items) + { + if (items is ICollection { Count: var count }) + { + this.EnsureCapacityExponential(this.LengthUnsafe + count); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T)); + foreach (var item in items) + this.DataUnsafe[index++] = item; + } + else + { + foreach (var item in items) + this.Insert(index++, item); + } + } + + /// + public void InsertRange(int index, Span items) + { + this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T)); + foreach (var item in items) + this.DataUnsafe[index++] = item; + } + + /// + /// Removes the element at the given index. + /// + /// The index. + /// Whether to skip calling the destroyer function. + public void RemoveAt(int index, bool skipDestroyer = false) + { + this.EnsureIndex(index); + var num = this.LengthUnsafe - index - 1; + if (!skipDestroyer) + this.destroyer?.Invoke(&this.DataUnsafe[index]); + + Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T)); + } + + /// + void IList.RemoveAt(int index) => this.RemoveAt(index); + + /// + void IList.RemoveAt(int index) => this.RemoveAt(index); + + /// + /// Sets the capacity exactly as requested. + /// + /// New capacity. + /// Whether the capacity has been changed. + /// If is less than . + /// If memory for the requested capacity cannot be allocated. + public bool SetCapacity(int capacity) + { + if (capacity < this.LengthUnsafe) + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, null); + + if (capacity == this.LengthUnsafe) + { + if (capacity == 0 && this.DataUnsafe is not null) + { + ImGuiNative.igMemFree(this.DataUnsafe); + this.DataUnsafe = null; + } + + return false; + } + + var oldAlloc = this.DataUnsafe; + var oldSpan = new Span(oldAlloc, this.CapacityUnsafe); + + var newAlloc = (T*)(capacity == 0 + ? null + : ImGuiNative.igMemAlloc(checked((uint)(capacity * sizeof(T))))); + + if (newAlloc is null && capacity > 0) + throw new OutOfMemoryException(); + + var newSpan = new Span(newAlloc, capacity); + + if (!oldSpan.IsEmpty && !newSpan.IsEmpty) + oldSpan[..this.LengthUnsafe].CopyTo(newSpan); +// #if DEBUG +// new Span(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC); +// #endif + + if (oldAlloc != null) + ImGuiNative.igMemFree(oldAlloc); + + this.DataUnsafe = newAlloc; + this.CapacityUnsafe = capacity; + + return true; + } + + /// + void ICollection.Add(T item) => this.Add(in item); + + /// + bool ICollection.Contains(T item) => this.Contains(in item); + + /// + void ICollection.CopyTo(Array array, int index) + { + if (index < 0) + { + throw new ArgumentOutOfRangeException( + nameof(index), + index, + $"{nameof(index)} is less than 0."); + } + + if (array.Length - index < this.LengthUnsafe) + { + throw new ArgumentException( + "The number of elements in the source ImVectorWrapper is greater than the available space from arrayIndex to the end of the destination array.", + nameof(array)); + } + + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + array.SetValue(this.DataUnsafe[i], index); + } + + /// + bool ICollection.Remove(T item) => this.Remove(in item); + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + int IList.Add(object? value) + { + this.Add(value is null ? default : (T)value); + return this.LengthUnsafe - 1; + } + + /// + bool IList.Contains(object? value) => this.Contains(value is null ? default : (T)value); + + /// + int IList.IndexOf(object? value) => this.IndexOf(value is null ? default : (T)value); + + /// + void IList.Insert(int index, object? value) => this.Insert(index, value is null ? default : (T)value); + + /// + void IList.Remove(object? value) => this.Remove(value is null ? default : (T)value); + + /// + int IList.IndexOf(T item) => this.IndexOf(in item); + + /// + void IList.Insert(int index, T item) => this.Insert(index, in item); + + private int EnsureIndex(int i) => i >= 0 && i < this.LengthUnsafe ? i : throw new IndexOutOfRangeException(); +} diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index 2fb735640..5712f419b 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -1,6 +1,5 @@ -using System; - using Serilog; +using Serilog.Core; using Serilog.Events; namespace Dalamud.Logging.Internal; @@ -33,6 +32,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Verbose(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values); @@ -42,6 +42,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Verbose(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); @@ -50,6 +51,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Debug(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values); @@ -59,6 +61,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Debug(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); @@ -67,6 +70,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Information(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, null, values); @@ -76,6 +80,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Information(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); @@ -84,6 +89,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Warning(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values); @@ -93,6 +99,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Warning(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); @@ -101,6 +108,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Error(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, null, values); @@ -110,6 +118,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Error(Exception? exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); @@ -118,6 +127,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Fatal(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values); @@ -127,9 +137,11 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Fatal(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); + [MessageTemplateFormatMethod("messageTemplate")] private void WriteLog( LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) { diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index b77f71d08..92dfe5dd7 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -137,6 +137,7 @@ internal static partial class NativeFunctions /// /// MB_* from winuser. /// + [Flags] public enum MessageBoxType : uint { ///