Dalamud/Dalamud/Interface/Utility/ImVectorWrapper.cs
2023-11-26 22:58:26 +01:00

687 lines
24 KiB
C#

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();
}