mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
Miscellaneous improvements (#1537)
This commit is contained in:
parent
a6ea4aa56a
commit
7a0de45f87
15 changed files with 1136 additions and 279 deletions
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
687
Dalamud/Interface/Utility/ImVectorWrapper.cs
Normal file
687
Dalamud/Interface/Utility/ImVectorWrapper.cs
Normal 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();
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ internal static partial class NativeFunctions
|
|||
/// <summary>
|
||||
/// MB_* from winuser.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MessageBoxType : uint
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue