Miscellaneous improvements (#1537)

This commit is contained in:
srkizer 2023-11-27 06:58:26 +09:00 committed by GitHub
parent a6ea4aa56a
commit 7a0de45f87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1136 additions and 279 deletions

View file

@ -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
/// <summary>
/// Event that occurs when dalamud configuration is saved.
/// </summary>
public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved;
public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved;
/// <summary>
/// Gets or sets a list of muted works.
/// </summary>
public List<string> BadWords { get; set; }
public List<string>? BadWords { get; set; }
/// <summary>
/// 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
/// <summary>
/// Gets or sets the language code to load Dalamud localization with.
/// </summary>
public string LanguageOverride { get; set; } = null;
public string? LanguageOverride { get; set; } = null;
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastVersion { get; set; } = null;
public string? LastVersion { get; set; } = null;
/// <summary>
/// Gets or sets a value indicating the last seen FTUE version.
@ -84,7 +85,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastChangelogMajorMinor { get; set; } = null;
public string? LastChangelogMajorMinor { get; set; } = null;
/// <summary>
/// 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.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; }
/// <summary>
@ -266,7 +268,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary>
public string DalamudBetaKind { get; set; }
public string? DalamudBetaKind { get; set; }
/// <summary>
/// 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<ReliableFileStorage>.Get().WriteAllText(
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));

View file

@ -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<FontTableEntry>(data, this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf<FontTableHeader>() + (Marshal.SizeOf<FontTableEntry>() * 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<KerningTableEntry>(data, this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf<KerningTableHeader>() + (Marshal.SizeOf<KerningTableEntry>() * i)));
}
@ -51,6 +50,14 @@ public class FdtReader
/// </summary>
public List<KerningTableEntry> Distances { get; init; } = new();
/// <summary>
/// Finds the glyph index for the corresponding codepoint.
/// </summary>
/// <param name="codepoint">Unicode codepoint (UTF-32 value).</param>
/// <returns>Corresponding index, or a negative number according to <see cref="List{T}.BinarySearch(int,int,T,System.Collections.Generic.IComparer{T}?)"/>.</returns>
public int FindGlyphIndex(int codepoint) =>
this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) });
/// <summary>
/// Finds glyph definition for corresponding codepoint.
/// </summary>
@ -58,7 +65,7 @@ public class FdtReader
/// <returns>Corresponding FontTableEntry, or null if not found.</returns>
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<T>(byte[] data, int offset)
{
var len = Marshal.SizeOf<T>();
if (offset + len > data.Length)
throw new Exception("Data too short");
fixed (byte* ptr = data)
return Marshal.PtrToStructure<T>(new(ptr + offset));
}
private static int CodePointToUtf8Int32(int codepoint)
/// <summary>
/// Translates a UTF-32 codepoint to a <see cref="uint"/> containing a UTF-8 character.
/// </summary>
/// <param name="codepoint">The codepoint.</param>
/// <returns>The uint.</returns>
internal static int CodePointToUtf8Int32(int codepoint)
{
if (codepoint <= 0x7F)
{
@ -131,6 +133,16 @@ public class FdtReader
}
}
private static unsafe T StructureFromByteArray<T>(byte[] data, int offset)
{
var len = Marshal.SizeOf<T>();
if (offset + len > data.Length)
throw new Exception("Data too short");
fixed (byte* ptr = data)
return Marshal.PtrToStructure<T>(new(ptr + offset));
}
private static int Utf8Uint32ToCodePoint(int n)
{
if ((n & 0xFFFFFF80) == 0)
@ -252,7 +264,7 @@ public class FdtReader
/// Glyph table entry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct FontTableEntry : IComparable<FontTableEntry>
public struct FontTableEntry : IComparable<FontTableEntry>
{
/// <summary>
/// Mapping of texture channel index to byte index.
@ -367,7 +379,7 @@ public class FdtReader
/// Kerning table entry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct KerningTableEntry : IComparable<KerningTableEntry>
public struct KerningTableEntry : IComparable<KerningTableEntry>
{
/// <summary>
/// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character.

View file

@ -257,7 +257,7 @@ internal class GameFontManager : IServiceType
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
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);
}
/// <summary>
@ -269,7 +269,7 @@ internal class GameFontManager : IServiceType
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
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);
}
/// <summary>

View file

@ -1,5 +1,3 @@
using System;
namespace Dalamud.Interface.GameFonts;
/// <summary>
@ -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
/// <param name="family">Font family.</param>
/// <param name="size">Font size in points.</param>
/// <returns>Recommended GameFontFamilyAndSize.</returns>
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;
/// <summary>
/// Calculates the adjustment to width resulting fron Weight and SkewStrength.
/// </summary>
/// <param name="header">Font header.</param>
/// <param name="glyph">Glyph.</param>
/// <returns>Width adjustment in pixel unit.</returns>
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);
}
/// <summary>
@ -265,16 +263,8 @@ public struct GameFontStyle
/// <param name="reader">Font information.</param>
/// <param name="glyph">Glyph.</param>
/// <returns>Width adjustment in pixel unit.</returns>
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);
/// <inheritdoc/>
public override string ToString()

View file

@ -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;
/// <summary>
/// The default font size, in points.
/// </summary>
public const float DefaultFontSizePt = 12.0f;
/// <summary>
/// The default font size, in pixels.
/// </summary>
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.

View file

@ -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<IDataWindowWidget> orderedModules;

View file

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Linq;
namespace Dalamud.Interface.Internal.Windows;
namespace Dalamud.Interface.Internal.Windows.Data;
/// <summary>
/// Class representing a date window entry.

View file

@ -28,8 +28,14 @@ public class AddonLifecycleWidget : IDataWindowWidget
/// <inheritdoc/>
public void Load()
{
this.AddonLifecycle = Service<AddonLifecycle>.GetNullable();
if (this.AddonLifecycle is not null) this.Ready = true;
Service<AddonLifecycle>
.GetAsync()
.ContinueWith(
r =>
{
this.AddonLifecycle = r.Result;
this.Ready = true;
});
}
/// <inheritdoc/>

View file

@ -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;
}

View file

@ -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;

View file

@ -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<InterfaceManager>.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;

View file

@ -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
/// </summary>
public static float GlobalScale { get; private set; }
/// <summary>
/// Gets a value indicating whether ImGui is initialized and ready for use.<br />
/// This does not necessarily mean you can call drawing functions.
/// </summary>
public static unsafe bool IsImGuiInitialized =>
ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null;
/// <summary>
/// Gets the global Dalamud scale; even available before drawing is ready.<br />
/// If you are sure that drawing is ready, at the point of using this, use <see cref="GlobalScale"/> instead.
/// </summary>
public static float GlobalScaleSafe =>
IsImGuiInitialized ? ImGui.GetIO().FontGlobalScale : Service<DalamudConfiguration>.Get().GlobalUiScale;
/// <summary>
/// Check if the current ImGui window is on the main viewport.
/// Only valid within a window.
@ -174,6 +189,47 @@ public static class ImGuiHelpers
}
}
/// <summary>
/// Unscales fonts after they have been rendered onto atlas.
/// </summary>
/// <param name="fontPtr">Font to scale.</param>
/// <param name="scale">Scale.</param>
/// <param name="round">If a positive number is given, numbers will be rounded to this.</param>
public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f)
{
Func<float, float> 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<ImFontGlyphHotDataReal>(
(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<ImFontGlyphReal>((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<ImFontKerningPair>((void*)font->KerningPairs.Data, font->KerningPairs.Size))
kp.AdvanceXAdjustment = rounder(kp.AdvanceXAdjustment * scale);
foreach (ref var fkp in new Span<float>((void*)font->FrequentKerningPairs.Data, font->FrequentKerningPairs.Size))
fkp = rounder(fkp * scale);
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
@ -183,71 +239,110 @@ public static class ImGuiHelpers
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
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);
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
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<int>();
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();
}
/// <summary>
@ -302,21 +397,35 @@ public static class ImGuiHelpers
/// Center the ImGui cursor for a certain text.
/// </summary>
/// <param name="text">The text to center for.</param>
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);
/// <summary>
/// Center the ImGui cursor for an item with a certain width.
/// </summary>
/// <param name="itemWidth">The width to center for.</param>
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));
/// <summary>
/// Determines whether <paramref name="ptr"/> is empty.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null;
/// <summary>
/// Determines whether <paramref name="ptr"/> is not null and loaded.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
public static unsafe bool IsNotNullAndLoaded(this ImFontPtr ptr) => ptr.NativePtr != null && ptr.IsLoaded();
/// <summary>
/// Determines whether <paramref name="ptr"/> is empty.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null;
/// <summary>
/// Get data needed for each new frame.
@ -330,19 +439,57 @@ public static class ImGuiHelpers
/// ImFontGlyph the correct version.
/// </summary>
[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.
/// </summary>
[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);
}
}
}

View file

@ -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;
/// <summary>
/// Utility methods for <see cref="ImVectorWrapper{T}"/>.
/// </summary>
public static class ImVectorWrapper
{
/// <summary>
/// Creates a new instance of the <see cref="ImVectorWrapper{T}"/> struct, initialized with
/// <paramref name="sourceEnumerable"/>.<br />
/// You must call <see cref="ImVectorWrapper{T}.Dispose"/> after use.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="sourceEnumerable">The initial data.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
/// <param name="minCapacity">The minimum capacity of the new vector.</param>
/// <returns>The new wrapped vector, that has to be disposed after use.</returns>
public static ImVectorWrapper<T> CreateFromEnumerable<T>(
IEnumerable<T> sourceEnumerable,
ImVectorWrapper<T>.ImGuiNativeDestroyDelegate? destroyer = null,
int minCapacity = 0)
where T : unmanaged
{
var res = new ImVectorWrapper<T>(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<T> 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;
}
}
/// <summary>
/// Creates a new instance of the <see cref="ImVectorWrapper{T}"/> struct, initialized with
/// <paramref name="sourceSpan"/>.<br />
/// You must call <see cref="ImVectorWrapper{T}.Dispose"/> after use.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="sourceSpan">The initial data.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
/// <param name="minCapacity">The minimum capacity of the new vector.</param>
/// <returns>The new wrapped vector, that has to be disposed after use.</returns>
public static ImVectorWrapper<T> CreateFromSpan<T>(
ReadOnlySpan<T> sourceSpan,
ImVectorWrapper<T>.ImGuiNativeDestroyDelegate? destroyer = null,
int minCapacity = 0)
where T : unmanaged
{
var res = new ImVectorWrapper<T>(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;
}
}
/// <summary>
/// Wraps <see cref="ImFontAtlas.ConfigData"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImFontConfig> ConfigDataWrapped(this ImFontAtlasPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->ConfigData, ImGuiNative.ImFontConfig_destroy);
/// <summary>
/// Wraps <see cref="ImFontAtlas.Fonts"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImFontPtr> FontsWrapped(this ImFontAtlasPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->Fonts, x => ImGuiNative.ImFont_destroy(x->NativePtr));
/// <summary>
/// Wraps <see cref="ImFontAtlas.Textures"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImFontAtlasTexture> TexturesWrapped(this ImFontAtlasPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->Textures);
/// <summary>
/// Wraps <see cref="ImFont.Glyphs"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImGuiHelpers.ImFontGlyphReal> GlyphsWrapped(this ImFontPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->Glyphs);
/// <summary>
/// Wraps <see cref="ImFont.IndexedHotData"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImGuiHelpers.ImFontGlyphHotDataReal> IndexedHotDataWrapped(this ImFontPtr obj)
=> obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->IndexedHotData);
/// <summary>
/// Wraps <see cref="ImFont.IndexLookup"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ushort> IndexLookupWrapped(this ImFontPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->IndexLookup);
}
/// <summary>
/// Wrapper for ImVector.
/// </summary>
/// <typeparam name="T">Contained type.</typeparam>
public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDisposable
where T : unmanaged
{
private ImVector* vector;
private ImGuiNativeDestroyDelegate? destroyer;
/// <summary>
/// Initializes a new instance of the <see cref="ImVectorWrapper{T}"/> struct.<br />
/// If <paramref name="ownership"/> is set to true, you must call <see cref="Dispose"/> after use,
/// and the underlying memory for <see cref="ImVector"/> must have been allocated using
/// <see cref="ImGuiNative.igMemAlloc"/>. Otherwise, it will crash.
/// </summary>
/// <param name="vector">The underlying vector.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
/// <param name="ownership">Whether this wrapper owns the vector.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImVectorWrapper{T}"/> struct.<br />
/// You must call <see cref="Dispose"/> after use.
/// </summary>
/// <param name="initialCapacity">The initial capacity.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
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;
}
}
/// <summary>
/// Destroy callback for items.
/// </summary>
/// <param name="self">Pointer to self.</param>
public delegate void ImGuiNativeDestroyDelegate(T* self);
/// <summary>
/// Gets the raw vector.
/// </summary>
public ImVector* RawVector => this.vector;
/// <summary>
/// Gets a <see cref="Span{T}"/> view of the underlying ImVector{T}, for the range of <see cref="Length"/>.
/// </summary>
public Span<T> DataSpan => new(this.DataUnsafe, this.LengthUnsafe);
/// <summary>
/// Gets a <see cref="Span{T}"/> view of the underlying ImVector{T}, for the range of <see cref="Capacity"/>.
/// </summary>
public Span<T> StorageSpan => new(this.DataUnsafe, this.CapacityUnsafe);
/// <summary>
/// Gets a value indicating whether this <see cref="ImVectorWrapper{T}"/> is disposed.
/// </summary>
public bool IsDisposed => this.vector is null;
/// <summary>
/// Gets a value indicating whether this <see cref="ImVectorWrapper{T}"/> has the ownership of the underlying
/// <see cref="ImVector"/>.
/// </summary>
public bool HasOwnership { get; private set; }
/// <summary>
/// Gets the underlying <see cref="ImVector"/>.
/// </summary>
public ImVector* Vector =>
this.vector is null ? throw new ObjectDisposedException(nameof(ImVectorWrapper<T>)) : this.vector;
/// <summary>
/// Gets the number of items contained inside the underlying ImVector{T}.
/// </summary>
public int Length => this.LengthUnsafe;
/// <summary>
/// Gets the number of items <b>that can be</b> contained inside the underlying ImVector{T}.
/// </summary>
public int Capacity => this.CapacityUnsafe;
/// <summary>
/// Gets the pointer to the first item in the data inside underlying ImVector{T}.
/// </summary>
public T* Data => this.DataUnsafe;
/// <summary>
/// Gets the reference to the number of items contained inside the underlying ImVector{T}.
/// </summary>
public ref int LengthUnsafe => ref *&this.Vector->Size;
/// <summary>
/// Gets the reference to the number of items <b>that can be</b> contained inside the underlying ImVector{T}.
/// </summary>
public ref int CapacityUnsafe => ref *&this.Vector->Capacity;
/// <summary>
/// Gets the reference to the pointer to the first item in the data inside underlying ImVector{T}.
/// </summary>
/// <remarks>This may be null, if <see cref="Capacity"/> is zero.</remarks>
public ref T* DataUnsafe => ref *(T**)&this.Vector->Data;
/// <inheritdoc cref="ICollection{T}.IsReadOnly"/>
public bool IsReadOnly => false;
/// <inheritdoc/>
int ICollection.Count => this.LengthUnsafe;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot { get; } = new();
/// <inheritdoc/>
int ICollection<T>.Count => this.LengthUnsafe;
/// <inheritdoc/>
int IReadOnlyCollection<T>.Count => this.LengthUnsafe;
/// <inheritdoc/>
bool IList.IsFixedSize => false;
/// <summary>
/// Gets the element at the specified index as a reference.
/// </summary>
/// <param name="index">Index of the item.</param>
/// <exception cref="IndexOutOfRangeException">If <paramref name="index"/> is out of range.</exception>
public ref T this[int index] => ref this.DataUnsafe[this.EnsureIndex(index)];
/// <inheritdoc/>
T IReadOnlyList<T>.this[int index] => this[index];
/// <inheritdoc/>
object? IList.this[int index]
{
get => this[index];
set => this[index] = value is null ? default : (T)value;
}
/// <inheritdoc/>
T IList<T>.this[int index]
{
get => this[index];
set => this[index] = value;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public IEnumerator<T> GetEnumerator()
{
foreach (var i in Enumerable.Range(0, this.LengthUnsafe))
yield return this[i];
}
/// <inheritdoc cref="ICollection{T}.Add"/>
public void Add(in T item)
{
this.EnsureCapacityExponential(this.LengthUnsafe + 1);
this.DataUnsafe[this.LengthUnsafe++] = item;
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void AddRange(IEnumerable<T> items)
{
if (items is ICollection { Count: var count })
this.EnsureCapacityExponential(this.LengthUnsafe + count);
foreach (var item in items)
this.Add(item);
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void AddRange(Span<T> items)
{
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
foreach (var item in items)
this.Add(item);
}
/// <inheritdoc cref="ICollection{T}.Clear"/>
public void Clear() => this.Clear(false);
/// <summary>
/// Clears this vector, optionally skipping destroyer invocation.
/// </summary>
/// <param name="skipDestroyer">Whether to skip destroyer invocation.</param>
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;
}
/// <inheritdoc cref="ICollection{T}.Contains"/>
public bool Contains(in T item) => this.IndexOf(in item) != -1;
/// <summary>
/// Size down the underlying ImVector{T}.
/// </summary>
/// <param name="reservation">Capacity to reserve.</param>
/// <returns>Whether the capacity has been changed.</returns>
public bool Compact(int reservation) => this.SetCapacity(Math.Max(reservation, this.LengthUnsafe));
/// <inheritdoc cref="ICollection{T}.CopyTo"/>
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<T> 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));
}
/// <summary>
/// Ensures that the capacity of this list is at least the specified <paramref name="capacity"/>.<br />
/// On growth, the new capacity exactly matches <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
/// <returns>Whether the capacity has been changed.</returns>
public bool EnsureCapacity(int capacity) => this.CapacityUnsafe < capacity && this.SetCapacity(capacity);
/// <summary>
/// Ensures that the capacity of this list is at least the specified <paramref name="capacity"/>.<br />
/// On growth, the new capacity may exceed <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
/// <returns>Whether the capacity has been changed.</returns>
public bool EnsureCapacityExponential(int capacity)
=> this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe)));
/// <summary>
/// Resizes the underlying array and fills with zeroes if grown.
/// </summary>
/// <param name="size">New size.</param>
/// <param name="defaultValue">New default value.</param>
/// <param name="skipDestroyer">Whether to skip calling destroyer function.</param>
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);
}
/// <inheritdoc cref="ICollection{T}.Remove"/>
public bool Remove(in T item)
{
var index = this.IndexOf(item);
if (index == -1)
return false;
this.RemoveAt(index);
return true;
}
/// <inheritdoc cref="IList{T}.IndexOf"/>
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;
}
/// <inheritdoc cref="IList{T}.Insert"/>
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;
}
/// <inheritdoc cref="List{T}.InsertRange"/>
public void InsertRange(int index, IEnumerable<T> 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);
}
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void InsertRange(int index, Span<T> 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;
}
/// <summary>
/// Removes the element at the given index.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="skipDestroyer">Whether to skip calling the destroyer function.</param>
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));
}
/// <inheritdoc/>
void IList<T>.RemoveAt(int index) => this.RemoveAt(index);
/// <inheritdoc/>
void IList.RemoveAt(int index) => this.RemoveAt(index);
/// <summary>
/// Sets the capacity exactly as requested.
/// </summary>
/// <param name="capacity">New capacity.</param>
/// <returns>Whether the capacity has been changed.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="capacity"/> is less than <see cref="Length"/>.</exception>
/// <exception cref="OutOfMemoryException">If memory for the requested capacity cannot be allocated.</exception>
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<T>(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<T>(newAlloc, capacity);
if (!oldSpan.IsEmpty && !newSpan.IsEmpty)
oldSpan[..this.LengthUnsafe].CopyTo(newSpan);
// #if DEBUG
// new Span<byte>(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;
}
/// <inheritdoc/>
void ICollection<T>.Add(T item) => this.Add(in item);
/// <inheritdoc/>
bool ICollection<T>.Contains(T item) => this.Contains(in item);
/// <inheritdoc/>
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<T> 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);
}
/// <inheritdoc/>
bool ICollection<T>.Remove(T item) => this.Remove(in item);
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
int IList.Add(object? value)
{
this.Add(value is null ? default : (T)value);
return this.LengthUnsafe - 1;
}
/// <inheritdoc/>
bool IList.Contains(object? value) => this.Contains(value is null ? default : (T)value);
/// <inheritdoc/>
int IList.IndexOf(object? value) => this.IndexOf(value is null ? default : (T)value);
/// <inheritdoc/>
void IList.Insert(int index, object? value) => this.Insert(index, value is null ? default : (T)value);
/// <inheritdoc/>
void IList.Remove(object? value) => this.Remove(value is null ? default : (T)value);
/// <inheritdoc/>
int IList<T>.IndexOf(T item) => this.IndexOf(in item);
/// <inheritdoc/>
void IList<T>.Insert(int index, T item) => this.Insert(index, in item);
private int EnsureIndex(int i) => i >= 0 && i < this.LengthUnsafe ? i : throw new IndexOutOfRangeException();
}

View file

@ -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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Verbose(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values);
@ -42,6 +42,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Debug(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values);
@ -59,6 +61,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Information(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Information, messageTemplate, null, values);
@ -76,6 +80,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Warning(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values);
@ -93,6 +99,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Error(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Error, messageTemplate, null, values);
@ -110,6 +118,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Fatal(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values);
@ -127,9 +137,11 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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)
{

View file

@ -137,6 +137,7 @@ internal static partial class NativeFunctions
/// <summary>
/// MB_* from winuser.
/// </summary>
[Flags]
public enum MessageBoxType : uint
{
/// <summary>