Merge branch 'Soreepeong-feature/enable-viewport-alpha'
Some checks failed
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Successful in 2s

This commit is contained in:
goaaats 2025-12-04 02:07:34 +01:00
commit 8dcbd52c22
5 changed files with 377 additions and 271 deletions

View file

@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler
private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
{
style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW);
exStyle =
(int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
if (flags.HasFlag(ImGuiViewportFlags.TopMost))
if ((flags & ImGuiViewportFlags.TopMost) != 0)
exStyle |= WS.WS_EX_TOPMOST;
if ((flags & ImGuiViewportFlags.NoInputs) != 0)
exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED;
}
}

View file

@ -8,6 +8,7 @@ using System.Text;
using Dalamud.Bindings.ImGui;
using Dalamud.Memory;
using Dalamud.Utility;
using Serilog;
@ -34,11 +35,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private readonly HCURSOR[] cursors;
private readonly WndProcDelegate wndProcDelegate;
private readonly bool[] imguiMouseIsDown;
private readonly nint platformNamePtr;
private ViewportHandler viewportHandler;
private int mouseButtonsDown;
private bool mouseTracked;
private long lastTime;
private nint iniPathPtr;
@ -64,7 +66,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
ImGuiBackendFlags.HasSetMousePos |
ImGuiBackendFlags.RendererHasViewports |
ImGuiBackendFlags.PlatformHasViewports;
ImGuiBackendFlags.PlatformHasViewports |
ImGuiBackendFlags.HasMouseHoveredViewport;
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
@ -74,8 +77,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
this.viewportHandler = new(this);
this.imguiMouseIsDown = new bool[5];
this.cursors = new HCURSOR[9];
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
@ -95,8 +96,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
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/>
public bool UpdateCursor { get; set; } = true;
@ -155,6 +154,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
public void NewFrame(int targetWidth, int targetHeight)
{
var io = ImGui.GetIO();
var focusedWindow = GetForegroundWindow();
io.DisplaySize.X = targetWidth;
io.DisplaySize.Y = targetHeight;
@ -168,9 +168,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors();
this.UpdateMousePos();
this.UpdateMouseData(focusedWindow);
this.ProcessKeyEventsWorkarounds();
this.ProcessKeyEventsWorkarounds(focusedWindow);
// 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
@ -224,6 +224,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
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_LBUTTONDBLCLK:
case WM.WM_RBUTTONDOWN:
@ -236,11 +270,10 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
var button = GetButton(msg, wParam);
if (io.WantCaptureMouse)
{
if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero)
if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
SetCapture(hWndCurrent);
io.MouseDown[button] = true;
this.imguiMouseIsDown[button] = true;
this.mouseButtonsDown |= 1 << button;
io.AddMouseButtonEvent(button, true);
return default(LRESULT);
}
@ -256,13 +289,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_XBUTTONUP:
{
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();
io.MouseDown[button] = false;
this.imguiMouseIsDown[button] = false;
io.AddMouseButtonEvent(button, false);
return default(LRESULT);
}
@ -272,7 +304,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEWHEEL:
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);
}
@ -280,7 +312,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEHWHEEL:
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);
}
@ -374,68 +406,86 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors();
break;
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
ReleaseCapture();
case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
io.AddFocusEvent(true);
break;
ImGui.GetIO().WantCaptureMouse = false;
ImGui.ClearWindowFocus();
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
io.AddFocusEvent(false);
// if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
// ReleaseCapture();
//
// ImGui.GetIO().WantCaptureMouse = false;
// ImGui.ClearWindowFocus();
break;
}
return null;
}
private void UpdateMousePos()
private void UpdateMouseData(HWND focusedWindow)
{
var io = ImGui.GetIO();
var pt = default(POINT);
// Depending on if Viewports are enabled, we have to change how we process
// the cursor position. If viewports are enabled, we pass the absolute cursor
// position to ImGui. Otherwise, we use the old method of passing client-local
// mouse position to ImGui.
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
var mouseScreenPos = default(POINT);
var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
var isAppFocused =
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)
{
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))
{
io.MousePos.X = pt.x;
io.MousePos.Y = pt.y;
}
else
{
io.MousePos.X = float.MinValue;
io.MousePos.Y = float.MinValue;
}
// (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
{
// 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)
// (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)
// (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;
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
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
{
if (io.WantSetMousePos)
{
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;
}
io.AddMouseViewportEvent(0);
}
}
private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
{
var hoveredHwnd = WindowFromPoint(mouseScreenPos);
return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
}
private bool UpdateMouseCursor()
{
var io = ImGui.GetIO();
@ -451,7 +501,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
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.
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
@ -480,7 +530,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{
// See: https://github.com/goatcorp/ImGuiScene/pull/13
// > 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++)
{
// Skip raising modifier keys if the game is focused.
@ -646,14 +696,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return;
var pio = ImGui.GetPlatformIO();
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;
}
ImGui.GetPlatformIO().Handle->Monitors.Free();
fixed (char* windowClassNamePtr = WindowClassName)
{
@ -693,59 +736,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// 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
var pio = ImGui.GetPlatformIO();
var numMonitors = GetSystemMetrics(SM.SM_CMONITORS);
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());
pio.Handle->Monitors.Resize(0);
// ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO();
// 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);
EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
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(
"Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}",
i,
"Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
monitor.MainPos,
monitor.MainSize,
monitor.WorkPos,
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)])]
@ -794,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
null);
}
if (data->Hwnd == 0)
Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
data->HwndOwned = true;
viewport.PlatformRequestResize = false;
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;

View file

@ -86,7 +86,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
: base(
"TitleScreenMenuOverlay",
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);

View file

@ -1,9 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CheapLoc;
@ -19,10 +16,13 @@ using Dalamud.Interface.Utility.Internal;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Windowing;
/// <summary>
@ -31,11 +31,15 @@ namespace Dalamud.Interface.Windowing;
public abstract class Window
{
private const float FadeInOutTime = 0.072f;
private const string AdditionsPopupName = "WindowSystemContextActions";
private static readonly ModuleLog Log = new("WindowSystem");
private static bool wasEscPressedLastFrame = false;
private readonly TitleBarButton additionsButton;
private readonly List<TitleBarButton> allButtons = [];
private bool internalLastIsOpen = false;
private bool internalIsOpen = false;
private bool internalIsPinned = false;
@ -72,6 +76,20 @@ public abstract class Window
this.WindowName = name;
this.Flags = flags;
this.ForceMainWindow = forceMainWindow;
this.additionsButton = new()
{
Icon = FontAwesomeIcon.Bars,
IconOffset = new Vector2(2.5f, 1),
Click = _ =>
{
this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(AdditionsPopupName);
},
Priority = int.MinValue,
AvailableClickthrough = true,
};
}
/// <summary>
@ -482,14 +500,12 @@ public abstract class Window
ImGuiP.GetCurrentWindow().InheritNoInputs = this.internalIsClickthrough;
}
// Not supported yet on non-main viewports
if ((this.internalIsPinned || this.internalIsClickthrough || this.internalAlpha.HasValue) &&
ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
if (ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
{
this.internalAlpha = null;
this.internalIsPinned = false;
this.internalIsClickthrough = false;
this.presetDirty = true;
if ((flags & ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs)
ImGui.GetWindowViewport().Flags |= ImGuiViewportFlags.NoInputs;
else
ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
}
if (this.hasError)
@ -513,7 +529,6 @@ public abstract class Window
}
}
const string additionsPopupName = "WindowSystemContextActions";
var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) &&
!flags.HasFlag(ImGuiWindowFlags.NoTitleBar);
var showAdditions = (this.AllowPinning || this.AllowClickthrough) &&
@ -524,13 +539,8 @@ public abstract class Window
{
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f);
if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove))
if (ImGui.BeginPopup(AdditionsPopupName, ImGuiWindowFlags.NoMove))
{
var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport();
if (!isAvailable)
ImGui.BeginDisabled();
if (this.internalIsClickthrough)
ImGui.BeginDisabled();
@ -578,21 +588,11 @@ public abstract class Window
this.presetDirty = true;
}
if (isAvailable)
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again by clicking the three dashes to disable clickthrough."));
}
else
{
ImGui.TextColored(ImGuiColors.DalamudGrey,
Loc.Localize("WindowSystemContextActionViewportDisclaimer",
"These features are only available if this window is inside the game window."));
}
if (!isAvailable)
ImGui.EndDisabled();
ImGui.TextColored(
ImGuiColors.DalamudGrey,
Loc.Localize(
"WindowSystemContextActionClickthroughDisclaimer",
"Open this menu again by clicking the three dashes to disable clickthrough."));
if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")))
printWindow = true;
@ -603,34 +603,15 @@ public abstract class Window
ImGui.PopStyleVar();
}
unsafe
if (flagsApplicableForTitleBarIcons)
{
var window = ImGuiP.GetCurrentWindow();
ImRect outRect;
ImGuiP.TitleBarRect(&outRect, window);
var additionsButton = new TitleBarButton
{
Icon = FontAwesomeIcon.Bars,
IconOffset = new Vector2(2.5f, 1),
Click = _ =>
{
this.internalIsClickthrough = false;
this.presetDirty = false;
ImGui.OpenPopup(additionsPopupName);
},
Priority = int.MinValue,
AvailableClickthrough = true,
};
if (flagsApplicableForTitleBarIcons)
{
this.DrawTitleBarButtons(window, flags, outRect,
showAdditions
? this.TitleBarButtons.Append(additionsButton)
: this.TitleBarButtons);
}
this.allButtons.Clear();
this.allButtons.EnsureCapacity(this.TitleBarButtons.Count + 1);
this.allButtons.AddRange(this.TitleBarButtons);
if (showAdditions)
this.allButtons.Add(this.additionsButton);
this.allButtons.Sort(static (a, b) => b.Priority - a.Priority);
this.DrawTitleBarButtons();
}
if (wasFocused)
@ -797,8 +778,11 @@ public abstract class Window
}
}
private unsafe void DrawTitleBarButtons(ImGuiWindowPtr window, ImGuiWindowFlags flags, ImRect titleBarRect, IEnumerable<TitleBarButton> buttons)
private unsafe void DrawTitleBarButtons()
{
var window = ImGuiP.GetCurrentWindow();
var flags = window.Flags;
var titleBarRect = window.TitleBarRect();
ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false);
var style = ImGui.GetStyle();
@ -833,26 +817,22 @@ public abstract class Window
var max = pos + new Vector2(fontSize, fontSize);
ImRect bb = new(pos, max);
var isClipped = !ImGuiP.ItemAdd(bb, id, null, 0);
bool hovered, held;
var pressed = false;
bool hovered, held, pressed;
if (this.internalIsClickthrough)
{
hovered = false;
held = false;
// ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves
if (ImGui.IsMouseHoveringRect(pos, max))
{
hovered = true;
var pad = ImGui.GetStyle().TouchExtraPadding;
var rect = new ImRect(pos - pad, max + pad);
hovered = rect.Contains(ImGui.GetMousePos());
// We can't use ImGui native functions here, because they don't work with clickthrough
if ((Windows.Win32.PInvoke.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0)
{
held = true;
pressed = true;
}
}
// Temporarily enable inputs
// This will be reset on next frame, and then enabled again if it is still being hovered
if (hovered && ImGui.GetWindowViewport().ID != ImGui.GetMainViewport().ID)
ImGui.GetWindowViewport().Flags &= ~ImGuiViewportFlags.NoInputs;
// We can't use ImGui native functions here, because they don't work with clickthrough
pressed = held = hovered && (GetKeyState(VK.VK_LBUTTON) & 0x8000) != 0;
}
else
{
@ -881,7 +861,7 @@ public abstract class Window
return pressed;
}
foreach (var button in buttons.OrderBy(x => x.Priority))
foreach (var button in this.allButtons)
{
if (this.internalIsClickthrough && !button.AvailableClickthrough)
return;
@ -1035,7 +1015,7 @@ public abstract class Window
/// <summary>
/// Gets or sets an action that is called when the button is clicked.
/// </summary>
public Action<ImGuiMouseButton> Click { get; set; }
public Action<ImGuiMouseButton>? Click { get; set; }
/// <summary>
/// Gets or sets the priority the button shall be shown in.

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;
/// <summary>
/// A structure representing a dynamic array for unmanaged types.
/// </summary>
public unsafe struct ImVector
{
public readonly int Size;
@ -15,23 +20,23 @@ public unsafe struct ImVector
Data = data;
}
public ref T Ref<T>(int index)
{
return ref Unsafe.AsRef<T>((byte*)Data + index * Unsafe.SizeOf<T>());
}
public readonly ref T Ref<T>(int index) => ref Unsafe.AsRef<T>((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
public IntPtr Address<T>(int index)
{
return (IntPtr)((byte*)Data + index * Unsafe.SizeOf<T>());
}
public readonly nint Address<T>(int index) => (nint)((byte*)this.Data + (index * Unsafe.SizeOf<T>()));
}
/// <summary>
/// A structure representing a dynamic array for unmanaged types.
/// </summary>
/// <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>
/// Initializes a new instance of the <see cref="ImVector{T}"/> struct with the specified size, capacity, and data pointer.
/// </summary>
@ -45,11 +50,6 @@ public unsafe struct ImVector<T> where T : unmanaged
this.data = data;
}
private int size;
private int capacity;
private unsafe T* data;
/// <summary>
/// Gets or sets the element at the specified index.
/// </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>
public T this[int index]
{
get
readonly get
{
if (index < 0 || index >= size)
{
if (index < 0 || index >= this.size)
throw new IndexOutOfRangeException();
}
return data[index];
return this.data[index];
}
set
{
if (index < 0 || index >= size)
{
if (index < 0 || index >= this.size)
throw new IndexOutOfRangeException();
}
data[index] = value;
this.data[index] = value;
}
}
/// <summary>
/// Gets a pointer to the first element of the vector.
/// </summary>
public readonly T* Data => data;
public readonly T* Data => this.data;
/// <summary>
/// Gets a pointer to the first element of the vector.
/// </summary>
public readonly T* Front => data;
public readonly T* Front => this.data;
/// <summary>
/// Gets a pointer to the last element of the vector.
/// </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>
/// Gets or sets the capacity of the vector.
/// </summary>
public int Capacity
{
readonly get => capacity;
readonly get => this.capacity;
set
{
if (capacity == value)
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, this.size, nameof(Capacity));
if (this.capacity == value)
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
{
int newSize = Math.Min(size, value);
T* newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
Buffer.MemoryCopy(data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
ImGui.MemFree(data);
data = newData;
size = newSize;
var newSize = Math.Min(this.size, value);
var newData = (T*)ImGui.MemAlloc((nuint)(value * sizeof(T)));
Buffer.MemoryCopy(this.data, newData, (nuint)(value * sizeof(T)), (nuint)(newSize * sizeof(T)));
ImGui.MemFree(this.data);
this.data = newData;
this.size = newSize;
}
capacity = value;
this.capacity = value;
// Clear the rest of the data
for (int i = size; i < capacity; i++)
{
data[i] = default;
}
new Span<T>(this.data + this.size, this.capacity - this.size).Clear();
}
}
/// <summary>
/// Gets the number of elements in the vector.
/// </summary>
public readonly int Size => size;
public readonly int Size => this.size;
/// <summary>
/// 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>
public void Grow(int newCapacity)
{
if (newCapacity > capacity)
{
Capacity = newCapacity * 2;
}
var newCapacity2 = this.capacity > 0 ? this.capacity + (this.capacity / 2) : 8;
this.Capacity = newCapacity2 > newCapacity ? newCapacity2 : newCapacity;
}
/// <summary>
@ -151,10 +141,8 @@ public unsafe struct ImVector<T> where T : unmanaged
/// <param name="size">The minimum capacity required.</param>
public void EnsureCapacity(int size)
{
if (size > capacity)
{
if (size > this.capacity)
Grow(size);
}
}
/// <summary>
@ -164,25 +152,46 @@ public unsafe struct ImVector<T> where T : unmanaged
public void Resize(int newSize)
{
EnsureCapacity(newSize);
size = newSize;
this.size = newSize;
}
/// <summary>
/// Clears all elements from the vector.
/// </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>
/// Adds an element to the end of the vector.
/// </summary>
/// <param name="value">The value to add.</param>
public void PushBack(T value)
[OverloadResolutionPriority(2)]
public void PushBack(in T value)
{
EnsureCapacity(size + 1);
data[size++] = value;
EnsureCapacity(this.size + 1);
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>
@ -190,48 +199,126 @@ public unsafe struct ImVector<T> where T : unmanaged
/// </summary>
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>
/// Frees the memory allocated for the vector.
/// </summary>
public void Free()
{
if (data != null)
if (this.data != null)
{
ImGui.MemFree(data);
data = null;
size = 0;
capacity = 0;
ImGui.MemFree(this.data);
this.data = null;
this.size = 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();
}
}