Update InputHandler to match changes in imgui_impl_win32.cpp

This commit is contained in:
Soreepeong 2025-08-12 16:18:49 +09:00
parent 40e63f2d9a
commit e5451c37af
3 changed files with 308 additions and 187 deletions

View file

@ -34,11 +34,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private readonly HCURSOR[] cursors; private readonly HCURSOR[] cursors;
private readonly WndProcDelegate wndProcDelegate; private readonly WndProcDelegate wndProcDelegate;
private readonly bool[] imguiMouseIsDown;
private readonly nint platformNamePtr; private readonly nint platformNamePtr;
private ViewportHandler viewportHandler; private ViewportHandler viewportHandler;
private int mouseButtonsDown;
private bool mouseTracked;
private long lastTime; private long lastTime;
private nint iniPathPtr; private nint iniPathPtr;
@ -64,7 +65,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
ImGuiBackendFlags.HasSetMousePos | ImGuiBackendFlags.HasSetMousePos |
ImGuiBackendFlags.RendererHasViewports | ImGuiBackendFlags.RendererHasViewports |
ImGuiBackendFlags.PlatformHasViewports; ImGuiBackendFlags.PlatformHasViewports |
ImGuiBackendFlags.HasMouseHoveredViewport;
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#"); this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr; io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
@ -74,8 +76,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
this.viewportHandler = new(this); this.viewportHandler = new(this);
this.imguiMouseIsDown = new bool[5];
this.cursors = new HCURSOR[9]; this.cursors = new HCURSOR[9];
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW); this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM); this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
@ -95,8 +95,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam); private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam);
private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam);
/// <inheritdoc/> /// <inheritdoc/>
public bool UpdateCursor { get; set; } = true; public bool UpdateCursor { get; set; } = true;
@ -155,6 +153,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
public void NewFrame(int targetWidth, int targetHeight) public void NewFrame(int targetWidth, int targetHeight)
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
var focusedWindow = GetForegroundWindow();
io.DisplaySize.X = targetWidth; io.DisplaySize.X = targetWidth;
io.DisplaySize.Y = targetHeight; io.DisplaySize.Y = targetHeight;
@ -168,9 +167,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors(); this.viewportHandler.UpdateMonitors();
this.UpdateMousePos(); this.UpdateMouseData(focusedWindow);
this.ProcessKeyEventsWorkarounds(); this.ProcessKeyEventsWorkarounds(focusedWindow);
// TODO: need to figure out some way to unify all this // TODO: need to figure out some way to unify all this
// The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues // The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues
@ -224,6 +223,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
switch (msg) switch (msg)
{ {
case WM.WM_MOUSEMOVE:
{
if (!this.mouseTracked)
{
var tme = new TRACKMOUSEEVENT
{
cbSize = (uint)sizeof(TRACKMOUSEEVENT),
dwFlags = TME.TME_LEAVE,
hwndTrack = hWndCurrent,
};
this.mouseTracked = TrackMouseEvent(&tme);
}
var mousePos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(hWndCurrent, &mousePos);
io.AddMousePosEvent(mousePos.x, mousePos.y);
break;
}
case WM.WM_MOUSELEAVE:
{
this.mouseTracked = false;
var mouseScreenPos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
ClientToScreen(hWndCurrent, &mouseScreenPos);
if (this.ViewportFromPoint(mouseScreenPos).IsNull)
{
var fltMax = ImGuiNative.GETFLTMAX();
io.AddMousePosEvent(-fltMax, -fltMax);
}
break;
}
case WM.WM_LBUTTONDOWN: case WM.WM_LBUTTONDOWN:
case WM.WM_LBUTTONDBLCLK: case WM.WM_LBUTTONDBLCLK:
case WM.WM_RBUTTONDOWN: case WM.WM_RBUTTONDOWN:
@ -236,11 +269,10 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
var button = GetButton(msg, wParam); var button = GetButton(msg, wParam);
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero) if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
SetCapture(hWndCurrent); SetCapture(hWndCurrent);
this.mouseButtonsDown |= 1 << button;
io.MouseDown[button] = true; io.AddMouseButtonEvent(button, true);
this.imguiMouseIsDown[button] = true;
return default(LRESULT); return default(LRESULT);
} }
@ -256,13 +288,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_XBUTTONUP: case WM.WM_XBUTTONUP:
{ {
var button = GetButton(msg, wParam); var button = GetButton(msg, wParam);
if (io.WantCaptureMouse && this.imguiMouseIsDown[button]) if (io.WantCaptureMouse)
{ {
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) this.mouseButtonsDown &= ~(1 << button);
if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
ReleaseCapture(); ReleaseCapture();
io.AddMouseButtonEvent(button, false);
io.MouseDown[button] = false;
this.imguiMouseIsDown[button] = false;
return default(LRESULT); return default(LRESULT);
} }
@ -272,7 +303,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEWHEEL: case WM.WM_MOUSEWHEEL:
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; io.AddMouseWheelEvent(0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
return default(LRESULT); return default(LRESULT);
} }
@ -280,7 +311,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEHWHEEL: case WM.WM_MOUSEHWHEEL:
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; io.AddMouseWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0);
return default(LRESULT); return default(LRESULT);
} }
@ -374,68 +405,86 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors(); this.viewportHandler.UpdateMonitors();
break; break;
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd: case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) io.AddFocusEvent(true);
ReleaseCapture(); break;
ImGui.GetIO().WantCaptureMouse = false; case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
ImGui.ClearWindowFocus(); io.AddFocusEvent(false);
// if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
// ReleaseCapture();
//
// ImGui.GetIO().WantCaptureMouse = false;
// ImGui.ClearWindowFocus();
break; break;
} }
return null; return null;
} }
private void UpdateMousePos() private void UpdateMouseData(HWND focusedWindow)
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
var pt = default(POINT);
// Depending on if Viewports are enabled, we have to change how we process var mouseScreenPos = default(POINT);
// the cursor position. If viewports are enabled, we pass the absolute cursor var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
// position to ImGui. Otherwise, we use the old method of passing client-local
// mouse position to ImGui. var isAppFocused =
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) focusedWindow != default
&& (focusedWindow == this.hWnd
|| IsChild(focusedWindow, this.hWnd)
|| !ImGui.FindViewportByPlatformHandle(focusedWindow).IsNull);
if (isAppFocused)
{ {
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
// When multi-viewports are enabled, all Dear ImGui positions are same as OS positions.
if (io.WantSetMousePos) if (io.WantSetMousePos)
{ {
SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y); var pos = new POINT((int)io.MousePos.X, (int)io.MousePos.Y);
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(this.hWnd, &pos);
SetCursorPos(pos.x, pos.y);
} }
if (GetCursorPos(&pt)) // (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
{ {
io.MousePos.X = pt.x; // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
io.MousePos.Y = pt.y; // (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
} // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
else // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
{ var mousePos = mouseScreenPos;
io.MousePos.X = float.MinValue; if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
io.MousePos.Y = float.MinValue; ClientToScreen(focusedWindow, &mousePos);
io.AddMousePosEvent(mousePos.x, mousePos.y);
} }
} }
// (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering.
// If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic.
// - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that)
// Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window
// for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported
// by the backend, and use its flawed heuristic to guess the viewport behind.
// - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target).
if (hasMouseScreenPos)
{
var viewport = this.ViewportFromPoint(mouseScreenPos);
io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u);
}
else else
{ {
if (io.WantSetMousePos) io.AddMouseViewportEvent(0);
{
pt.x = (int)io.MousePos.X;
pt.y = (int)io.MousePos.Y;
ClientToScreen(this.hWnd, &pt);
SetCursorPos(pt.x, pt.y);
}
if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
{
io.MousePos.X = pt.x;
io.MousePos.Y = pt.y;
}
else
{
io.MousePos.X = float.MinValue;
io.MousePos.Y = float.MinValue;
}
} }
} }
private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
{
var hoveredHwnd = WindowFromPoint(mouseScreenPos);
return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
}
private bool UpdateMouseCursor() private bool UpdateMouseCursor()
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
@ -451,7 +500,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return true; return true;
} }
private void ProcessKeyEventsWorkarounds() private void ProcessKeyEventsWorkarounds(HWND focusedWindow)
{ {
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one. // Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT)) if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
@ -480,7 +529,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{ {
// See: https://github.com/goatcorp/ImGuiScene/pull/13 // See: https://github.com/goatcorp/ImGuiScene/pull/13
// > GetForegroundWindow from winuser.h is a surprisingly expensive function. // > GetForegroundWindow from winuser.h is a surprisingly expensive function.
var isForeground = GetForegroundWindow() == this.hWnd; var isForeground = focusedWindow == this.hWnd;
for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++) for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++)
{ {
// Skip raising modifier keys if the game is focused. // Skip raising modifier keys if the game is focused.
@ -646,14 +695,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return; return;
var pio = ImGui.GetPlatformIO(); var pio = ImGui.GetPlatformIO();
ImGui.GetPlatformIO().Handle->Monitors.Free();
if (ImGui.GetPlatformIO().Handle->Monitors.Data != null)
{
// We allocated the platform monitor data in OnUpdateMonitors ourselves,
// so we have to free it ourselves to ImGui doesn't try to, or else it will crash
Marshal.FreeHGlobal(new IntPtr(ImGui.GetPlatformIO().Handle->Monitors.Data));
ImGui.GetPlatformIO().Handle->Monitors = default;
}
fixed (char* windowClassNamePtr = WindowClassName) fixed (char* windowClassNamePtr = WindowClassName)
{ {
@ -693,59 +735,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// Here we use a manual ImVector overload, free the existing monitor data, // Here we use a manual ImVector overload, free the existing monitor data,
// and allocate our own, as we are responsible for telling ImGui about monitors // and allocate our own, as we are responsible for telling ImGui about monitors
var pio = ImGui.GetPlatformIO(); var pio = ImGui.GetPlatformIO();
var numMonitors = GetSystemMetrics(SM.SM_CMONITORS); pio.Handle->Monitors.Resize(0);
var data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
if (pio.Handle->Monitors.Data != null)
Marshal.FreeHGlobal(new IntPtr(pio.Handle->Monitors.Data));
pio.Handle->Monitors = new(numMonitors, numMonitors, (ImGuiPlatformMonitor*)data.ToPointer());
// ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
// Marshal.FreeHGlobal(platformIO.Handle->Monitors.Data);
// int numMonitors = GetSystemMetrics(SystemMetric.SM_CMONITORS);
// nint data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
// platformIO.Handle->Monitors = new ImVector(numMonitors, numMonitors, data);
var monitorIndex = -1;
var enumfn = new MonitorEnumProcDelegate(
(hMonitor, _, _, _) =>
{
monitorIndex++;
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
ref var imMonitor = ref ImGui.GetPlatformIO().Monitors.Ref(monitorIndex);
imMonitor.MainPos = monitorLt;
imMonitor.MainSize = monitorRb - monitorLt;
imMonitor.WorkPos = workLt;
imMonitor.WorkSize = workRb - workLt;
imMonitor.DpiScale = 1f;
return true;
});
EnumDisplayMonitors(
default,
null,
(delegate* unmanaged<HMONITOR, HDC, RECT*, LPARAM, BOOL>)Marshal.GetFunctionPointerForDelegate(enumfn),
default);
Log.Information("Monitors set up!"); Log.Information("Monitors set up!");
for (var i = 0; i < numMonitors; i++) foreach (ref var monitor in pio.Handle->Monitors)
{ {
var monitor = pio.Handle->Monitors[i];
Log.Information( Log.Information(
"Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}", "Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
i,
monitor.MainPos, monitor.MainPos,
monitor.MainSize, monitor.MainSize,
monitor.WorkPos, monitor.WorkPos,
monitor.WorkSize); monitor.WorkSize);
} }
return;
[UnmanagedCallersOnly]
static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam)
{
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
var imMonitor = new ImGuiPlatformMonitor
{
MainPos = monitorLt,
MainSize = monitorRb - monitorLt,
WorkPos = workLt,
WorkSize = workRb - workLt,
DpiScale = 1f,
};
if ((info.dwFlags & MONITORINFOF_PRIMARY) != 0)
ImGui.GetPlatformIO().Monitors.PushFront(imMonitor);
else
ImGui.GetPlatformIO().Monitors.PushBack(imMonitor);
return true;
}
} }
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]

View file

@ -86,7 +86,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
: base( : base(
"TitleScreenMenuOverlay", "TitleScreenMenuOverlay",
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus |
ImGuiWindowFlags.NoDocking)
{ {
this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true); this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true);

View file

@ -1,7 +1,12 @@
using System.Runtime.CompilerServices; using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Dalamud.Bindings.ImGui; namespace Dalamud.Bindings.ImGui;
/// <summary>
/// A structure representing a dynamic array for unmanaged types.
/// </summary>
public unsafe struct ImVector public unsafe struct ImVector
{ {
public readonly int Size; public readonly int Size;
@ -15,23 +20,23 @@ public unsafe struct ImVector
Data = data; Data = data;
} }
public ref T Ref<T>(int index) public readonly ref T Ref<T>(int index) => ref Unsafe.AsRef<T>((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
{
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>());
}
public IntPtr Address<T>(int index) public readonly nint Address<T>(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
{
return (IntPtr)((byte*)Data + index * Unsafe.SizeOf<T>());
}
} }
/// <summary> /// <summary>
/// A structure representing a dynamic array for unmanaged types. /// A structure representing a dynamic array for unmanaged types.
/// </summary> /// </summary>
/// <typeparam name="T">The type of elements in the vector, must be unmanaged.</typeparam> /// <typeparam name="T">The type of elements in the vector, must be unmanaged.</typeparam>
public unsafe struct ImVector<T> where T : unmanaged [StructLayout(LayoutKind.Sequential)]
public unsafe struct ImVector<T> : IEnumerable<T>
where T : unmanaged
{ {
private int size;
private int capacity;
private T* data;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImVector{T}"/> struct with the specified size, capacity, and data pointer. /// Initializes a new instance of the <see cref="ImVector{T}"/> struct with the specified size, capacity, and data pointer.
/// </summary> /// </summary>
@ -45,11 +50,6 @@ public unsafe struct ImVector<T> where T : unmanaged
this.data = data; this.data = data;
} }
private int size;
private int capacity;
private unsafe T* data;
/// <summary> /// <summary>
/// Gets or sets the element at the specified index. /// Gets or sets the element at the specified index.
/// </summary> /// </summary>
@ -58,80 +58,72 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <exception cref="IndexOutOfRangeException">Thrown when the index is out of range.</exception> /// <exception cref="IndexOutOfRangeException">Thrown when the index is out of range.</exception>
public T this[int index] public T this[int index]
{ {
get readonly get
{ {
if (index < 0 || index >= size) if (index < 0 || index >= this.size)
{
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
} return this.data[index];
return data[index];
} }
set set
{ {
if (index < 0 || index >= size) if (index < 0 || index >= this.size)
{
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
} this.data[index] = value;
data[index] = value;
} }
} }
/// <summary> /// <summary>
/// Gets a pointer to the first element of the vector. /// Gets a pointer to the first element of the vector.
/// </summary> /// </summary>
public readonly T* Data => data; public readonly T* Data => this.data;
/// <summary> /// <summary>
/// Gets a pointer to the first element of the vector. /// Gets a pointer to the first element of the vector.
/// </summary> /// </summary>
public readonly T* Front => data; public readonly T* Front => this.data;
/// <summary> /// <summary>
/// Gets a pointer to the last element of the vector. /// Gets a pointer to the last element of the vector.
/// </summary> /// </summary>
public readonly T* Back => size > 0 ? data + size - 1 : null; public readonly T* Back => this.size > 0 ? this.data + this.size - 1 : null;
/// <summary> /// <summary>
/// Gets or sets the capacity of the vector. /// Gets or sets the capacity of the vector.
/// </summary> /// </summary>
public int Capacity public int Capacity
{ {
readonly get => capacity; readonly get => this.capacity;
set set
{ {
if (capacity == value) ArgumentOutOfRangeException.ThrowIfLessThan(value, this.size, nameof(Capacity));
{ if (this.capacity == value)
return; return;
}
if (data == null) if (this.data == null)
{ {
data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); this.data = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
} }
else else
{ {
int newSize = Math.Min(size, value); var newSize = Math.Min(this.size, value);
T* newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T))); var newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
Buffer.MemoryCopy(data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T))); Buffer.MemoryCopy(this.data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
ImGui.MemFree(data); ImGui.MemFree(this.data);
data = newData; this.data = newData;
size = newSize; this.size = newSize;
} }
capacity = value; this.capacity = value;
// Clear the rest of the data // Clear the rest of the data
for (int i = size; i < capacity; i++) new Span<T>(this.data + this.size, this.capacity - this.size).Clear();
{
data[i] = default;
}
} }
} }
/// <summary> /// <summary>
/// Gets the number of elements in the vector. /// Gets the number of elements in the vector.
/// </summary> /// </summary>
public readonly int Size => size; public readonly int Size => this.size;
/// <summary> /// <summary>
/// Grows the capacity of the vector to at least the specified value. /// Grows the capacity of the vector to at least the specified value.
@ -139,10 +131,8 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <param name="newCapacity">The new capacity.</param> /// <param name="newCapacity">The new capacity.</param>
public void Grow(int newCapacity) public void Grow(int newCapacity)
{ {
if (newCapacity > capacity) var newCapacity2 = this.capacity > 0 ? this.capacity + (this.capacity / 2) : 8;
{ this.Capacity = newCapacity2 > newCapacity ? newCapacity2 : newCapacity;
Capacity = newCapacity * 2;
}
} }
/// <summary> /// <summary>
@ -151,10 +141,8 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <param name="size">The minimum capacity required.</param> /// <param name="size">The minimum capacity required.</param>
public void EnsureCapacity(int size) public void EnsureCapacity(int size)
{ {
if (size > capacity) if (size > this.capacity)
{
Grow(size); Grow(size);
}
} }
/// <summary> /// <summary>
@ -164,25 +152,46 @@ public unsafe struct ImVector<T> where T : unmanaged
public void Resize(int newSize) public void Resize(int newSize)
{ {
EnsureCapacity(newSize); EnsureCapacity(newSize);
size = newSize; this.size = newSize;
} }
/// <summary> /// <summary>
/// Clears all elements from the vector. /// Clears all elements from the vector.
/// </summary> /// </summary>
public void Clear() public void Clear() => this.size = 0;
/// <summary>
/// Adds an element to the end of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
[OverloadResolutionPriority(1)]
public void PushBack(T value)
{ {
size = 0; this.EnsureCapacity(this.size + 1);
this.data[this.size++] = value;
} }
/// <summary> /// <summary>
/// Adds an element to the end of the vector. /// Adds an element to the end of the vector.
/// </summary> /// </summary>
/// <param name="value">The value to add.</param> /// <param name="value">The value to add.</param>
public void PushBack(T value) [OverloadResolutionPriority(2)]
public void PushBack(in T value)
{ {
EnsureCapacity(size + 1); EnsureCapacity(this.size + 1);
data[size++] = value; this.data[this.size++] = value;
}
/// <summary>
/// Adds an element to the front of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
public void PushFront(in T value)
{
if (this.size == 0)
this.PushBack(value);
else
this.Insert(0, value);
} }
/// <summary> /// <summary>
@ -190,48 +199,126 @@ public unsafe struct ImVector<T> where T : unmanaged
/// </summary> /// </summary>
public void PopBack() public void PopBack()
{ {
if (size > 0) if (this.size > 0)
{ {
size--; this.size--;
} }
} }
public ref T Insert(int index, in T v) {
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
this.EnsureCapacity(this.size + 1);
if (index < this.size)
{
Buffer.MemoryCopy(
this.data + index,
this.data + index + 1,
(this.size - index) * sizeof(T),
(this.size - index) * sizeof(T));
}
this.data[index] = v;
this.size++;
return ref this.data[index];
}
public Span<T> InsertRange(int index, ReadOnlySpan<T> v)
{
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, this.size, nameof(index));
this.EnsureCapacity(this.size + v.Length);
if (index < this.size)
{
Buffer.MemoryCopy(
this.data + index,
this.data + index + v.Length,
(this.size - index) * sizeof(T),
(this.size - index) * sizeof(T));
}
var dstSpan = new Span<T>(this.data + index, v.Length);
v.CopyTo(new(this.data + index, v.Length));
this.size += v.Length;
return dstSpan;
}
/// <summary> /// <summary>
/// Frees the memory allocated for the vector. /// Frees the memory allocated for the vector.
/// </summary> /// </summary>
public void Free() public void Free()
{ {
if (data != null) if (this.data != null)
{ {
ImGui.MemFree(data); ImGui.MemFree(this.data);
data = null; this.data = null;
size = 0; this.size = 0;
capacity = 0; this.capacity = 0;
} }
} }
public ref T Ref(int index) public readonly ref T Ref(int index)
{ {
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>()); return ref Unsafe.AsRef<T>((byte*)Data + (index * Unsafe.SizeOf<T>()));
} }
public ref TCast Ref<TCast>(int index) public readonly ref TCast Ref<TCast>(int index)
{ {
return ref Unsafe.AsRef<TCast>((byte*)Data + index * Unsafe.SizeOf<TCast>()); return ref Unsafe.AsRef<TCast>((byte*)Data + (index * Unsafe.SizeOf<TCast>()));
} }
public void* Address(int index) public readonly void* Address(int index)
{ {
return (byte*)Data + index * Unsafe.SizeOf<T>(); return (byte*)Data + (index * Unsafe.SizeOf<T>());
} }
public void* Address<TCast>(int index) public readonly void* Address<TCast>(int index)
{ {
return (byte*)Data + index * Unsafe.SizeOf<TCast>(); return (byte*)Data + (index * Unsafe.SizeOf<TCast>());
} }
public ImVector* ToUntyped() public readonly ImVector* ToUntyped()
{ {
return (ImVector*)Unsafe.AsPointer(ref this); return (ImVector*)Unsafe.AsPointer(ref Unsafe.AsRef(in this));
}
public readonly Span<T> AsSpan() => new(this.data, this.size);
public readonly Enumerator GetEnumerator() => new(this.data, this.data + this.size);
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
public struct Enumerator(T* begin, T* end) : IEnumerator<T>, IEnumerable<T>
{
private T* current = null;
public readonly ref T Current => ref *this.current;
readonly T IEnumerator<T>.Current => this.Current;
readonly object IEnumerator.Current => this.Current;
public bool MoveNext()
{
var next = this.current == null ? begin : this.current + 1;
if (next == end)
return false;
this.current = next;
return true;
}
public void Reset() => this.current = null;
public readonly Enumerator GetEnumerator() => new(begin, end);
readonly void IDisposable.Dispose()
{
}
readonly IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
readonly IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
} }
} }