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) private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
{ {
style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW); style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
exStyle = exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
(int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP; exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
if (flags.HasFlag(ImGuiViewportFlags.TopMost)) if ((flags & ImGuiViewportFlags.TopMost) != 0)
exStyle |= WS.WS_EX_TOPMOST; 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.Bindings.ImGui;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Utility;
using Serilog; using Serilog;
@ -34,11 +35,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 +66,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 +77,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 +96,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 +154,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 +168,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 +224,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 +270,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 +289,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 +304,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 +312,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 +406,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; {
io.MousePos.Y = pt.y; // 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.)
else // 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.)
io.MousePos.X = float.MinValue; var mousePos = mouseScreenPos;
io.MousePos.Y = float.MinValue; 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 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 +501,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 +530,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 +696,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 +736,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)])]
@ -794,6 +828,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
null); null);
} }
if (data->Hwnd == 0)
Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
data->HwndOwned = true; data->HwndOwned = true;
viewport.PlatformRequestResize = false; viewport.PlatformRequestResize = false;
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd; viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;

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