Files
tooll3--t3/Editor/App/ImGuiDx11RenderForm.cs
2026-07-13 13:13:17 +08:00

399 lines
14 KiB
C#

using System.Windows.Forms;
using ImGuiNET;
using SharpDX.Windows;
using T3.Editor.Gui.UiHelpers;
using T3.SystemUi;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo
namespace T3.Editor.App;
/// <summary>
/// A RenderForm class that maps windows RenderForm events and to ImGui
/// </summary>
internal class ImGuiDx11RenderForm : RenderForm
{
internal static IWindowsFormsMessageHandler[] InputMethods = [];
internal ImGuiDx11RenderForm(string title): base(title)
{
AllowDrop = true;
DragEnter += OnDragEnter;
DragDrop += OnDragDrop;
DragOver += OnDragOver;
DragLeave += OnDragLeave;
MouseMove += (_, e) =>
{
if (this != ProgramWindows.Viewer?.Form) // Ignore mouse updates from Viewer
{
ImGui.GetIO().MousePos = new System.Numerics.Vector2(e.X, e.Y);
}
};
}
#region WM Message Ids
private const int WM_LBUTTONDOWN = 0x0201;
private const int WM_LBUTTONUP = 0x0202;
private const int WM_LBUTTONDBLCLK = 0x0203;
private const int WM_RBUTTONDOWN = 0x0204;
private const int WM_RBUTTONUP = 0x0205;
private const int WM_RBUTTONDBLCLK = 0x0206;
private const int WM_MBUTTONDOWN = 0x0207;
private const int WM_MBUTTONUP = 0x0208;
private const int WM_MBUTTONDBLCLK = 0x0209;
private const int WM_MOUSEWHEEL = 0x020A;
private const int WM_MOUSEHWHEEL = 0x020E;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
private const int WM_KEYUP = 0x0101;
private const int WM_SYSKEYUP = 0x0105;
private const int WM_CHAR = 0x0102;
private const int WM_SETCURSOR = 0x0020;
private const int WM_SETFOCUS = 0x0007;
private const int WM_ACTIVATEAPP = 0x001C;
#endregion
#region VK constants
private const int VK_SHIFT = 0x10;
private const int VK_CONTROL = 0x11;
private const int VK_ALT = 0x12;
#endregion
public static event Action<string[], Vector2> FilesDropped;
private void OnDragEnter(object s, DragEventArgs e)
{
if (this == ProgramWindows.Viewer?.Form)
{
e.Effect = DragDropEffects.None;
return;
} // optional
if (e.Data == null)
{
return;
}
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop) ||
e.Data.GetDataPresent(DataFormats.UnicodeText)
? DragDropEffects.Copy
: DragDropEffects.None;
DragAndDropHandling.StartExternalDrag(DragAndDropHandling.DragTypes.ExternalFile,
"External Files");
}
private static void OnDragLeave(object s, EventArgs eventArgs)
{
DragAndDropHandling.CancelExternalDrag();
}
private void OnDragDrop(object s, DragEventArgs e)
{
if (this == ProgramWindows.Viewer?.Form || e.Data == null)
return; // optional
var p = PointToClient(new System.Drawing.Point(e.X, e.Y));
var pos = new Vector2(p.X, p.Y);
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
var files = (string[]) e.Data.GetData(DataFormats.FileDrop)!;
FilesDropped?.Invoke(files, pos);
DragAndDropHandling.CompleteExternalDrop(DragAndDropHandling.DragTypes.ExternalFile,
string.Join("|", files));
}
else if (e.Data.GetDataPresent(DataFormats.UnicodeText))
{
var t = ((string) e.Data.GetData(DataFormats.UnicodeText)!).Trim('"');
if (System.IO.Path.IsPathRooted(t))
{
DragAndDropHandling.CompleteExternalDrop(DragAndDropHandling.DragTypes.ExternalFile, t);
FilesDropped?.Invoke(new[] {t}, pos);
}
}
}
private void OnDragOver(object s, DragEventArgs e)
{
var p = PointToClient(new System.Drawing.Point(e.X, e.Y));
ImGui.GetIO().MousePos = new System.Numerics.Vector2(p.X, p.Y);
e.Effect = DragDropEffects.Copy;
}
protected override void WndProc(ref System.Windows.Forms.Message m)
{
try
{
var filterAltKeyToPreventFocusLoss =
(m.Msg == WM_SYSKEYDOWN || m.Msg == WM_SYSKEYUP) && (int) m.WParam == VK_ALT;
if (!filterAltKeyToPreventFocusLoss)
base.WndProc(ref m);
// The form's HWND can receive messages before WindowsUiContentDrawer.Initialize()
// calls ImGui.CreateContext(). ImGui 1.91 hard-asserts on GetIO() when no context
// exists, where 1.89 was lax. Bail out for those very early messages.
if (ImGui.GetCurrentContext() == IntPtr.Zero)
return;
foreach (var inputMethod in InputMethods)
inputMethod.ProcessMessage(m);
var isViewer = this == ProgramWindows.Viewer?.Form;
ImGuiIOPtr io = ImGui.GetIO();
switch (m.Msg)
{
case WM_LBUTTONDOWN:
case WM_LBUTTONDBLCLK:
case WM_RBUTTONDOWN:
case WM_RBUTTONDBLCLK:
case WM_MBUTTONDOWN:
case WM_MBUTTONDBLCLK:
{
if (isViewer)
return;
int button = 0;
if (m.Msg == WM_LBUTTONDOWN || m.Msg == WM_LBUTTONDBLCLK) button = 0;
if (m.Msg == WM_RBUTTONDOWN || m.Msg == WM_RBUTTONDBLCLK) button = 1;
if (m.Msg == WM_MBUTTONDOWN || m.Msg == WM_MBUTTONDBLCLK) button = 2;
// TODO
//if (!ImGui.IsAnyMouseDown() && ::GetCapture() == NULL)
// ::SetCapture(hwnd);
io.MouseDown[button] = true;
return;
}
case WM_LBUTTONUP:
case WM_RBUTTONUP:
case WM_MBUTTONUP:
{
int button = 0;
if (m.Msg == WM_LBUTTONUP) button = 0;
if (m.Msg == WM_RBUTTONUP) button = 1;
if (m.Msg == WM_MBUTTONUP) button = 2;
io.MouseDown[button] = false;
// TODO
//if (!ImGui::IsAnyMouseDown() && ::GetCapture() == hwnd)
// ::ReleaseCapture();
return;
}
case WM_MOUSEWHEEL:
case WM_MOUSEHWHEEL:
{
MouseWheelPanning.ProcessMouseWheelInput(m, io);
return;
}
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
HandleKeyEvent(io, (int)m.WParam, down: true);
return;
case WM_KEYUP:
case WM_SYSKEYUP:
HandleKeyEvent(io, (int)m.WParam, down: false);
return;
case WM_CHAR:
// You can also use ToAscii()+GetKeyboardState() to retrieve characters.
if ((int) m.WParam > 0 && (int) m.WParam < 0x10000)
io.AddInputCharacter((ushort) m.WParam);
return;
case WM_SETCURSOR:
if ((((int) m.LParam & 0xFFFF) == 1) && UpdateMouseCursor())
m.Result = 1;
return;
case WM_SETFOCUS:
io.AddFocusEvent(true);
SyncModifiersFromSystemState(io);
break;
case WM_ACTIVATEAPP:
if (m.WParam.ToInt64() == 0) /* Being deactivated */
{
// Clear all key state to avoid stuck keys when focus is lost
// (especially Alt, which can desync via Alt+Tab).
io.AddFocusEvent(false);
io.ClearInputKeys();
KeyHandler.ReleaseAllKeys();
}
else
{
SyncModifiersFromSystemState(io);
}
break;
}
}
catch (NullReferenceException)
{
Log.Warning("Detected invalid event message that would trigger null-reference exception");
}
}
/// <summary>
/// Forwards a single Win32 keyboard event to ImGui's new input system,
/// updates ImGui's modifier flags, and keeps TiXL's <see cref="KeyHandler"/>
/// state in sync for the Alt key (used by the WndProc filter above).
/// </summary>
private static void HandleKeyEvent(ImGuiIOPtr io, int vkCode, bool down)
{
// Modifier flags — set both the dedicated mod event (used by ImGui shortcut
// matching) and the legacy bool fields (used by InputText etc.).
switch (vkCode)
{
case VK_SHIFT:
io.KeyShift = down;
io.AddKeyEvent(ImGuiKey.ModShift, down);
break;
case VK_CONTROL:
io.KeyCtrl = down;
io.AddKeyEvent(ImGuiKey.ModCtrl, down);
break;
case VK_ALT:
io.KeyAlt = down;
io.AddKeyEvent(ImGuiKey.ModAlt, down);
if (down)
KeyHandler.SetKeyDown(Key.Alt);
else
KeyHandler.SetKeyUp(Key.Alt);
break;
}
// Forward the actual key as an ImGuiKey event.
var imguiKey = ((Key)vkCode).ToImGuiKey();
if (imguiKey != ImGuiKey.None)
io.AddKeyEvent(imguiKey, down);
}
/// <summary>
/// Releases modifiers that are no longer physically held, according to Windows.
/// Key-up events are lost while the app is unfocused or halted at a debugger
/// breakpoint, so modifiers are re-checked whenever focus returns.
///
/// This only ever *releases* keys: GetKeyState reflects the thread's synchronous
/// key state, which can report a stale "down" right after refocusing (the release
/// was delivered to the previously focused app). Synthesizing a key-down from it
/// would stick the modifier with no key-up ever arriving. A genuinely held
/// modifier re-registers through its auto-repeat WM_KEYDOWN messages.
/// </summary>
private static void SyncModifiersFromSystemState(ImGuiIOPtr io)
{
if (!IsVkDown(VK_SHIFT))
{
io.KeyShift = false;
io.AddKeyEvent(ImGuiKey.ModShift, false);
KeyHandler.SetKeyUp(Key.ShiftKey);
}
if (!IsVkDown(VK_CONTROL))
{
io.KeyCtrl = false;
io.AddKeyEvent(ImGuiKey.ModCtrl, false);
KeyHandler.SetKeyUp(Key.CtrlKey);
}
if (!IsVkDown(VK_ALT))
{
io.KeyAlt = false;
io.AddKeyEvent(ImGuiKey.ModAlt, false);
KeyHandler.SetKeyUp(Key.Alt);
}
}
private static bool IsVkDown(int vkCode) => (GetKeyState(vkCode) & 0x8000) != 0;
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern short GetKeyState(int nVirtKey);
/// <summary>
/// Applies the cursor ImGui requested this frame right away. Windows only sends
/// WM_SETCURSOR on the next mouse movement, so waiting for it makes the pointer
/// shape trail one movement step behind the hover state — very noticeable on
/// small drag handles and splitters.
/// </summary>
internal void ApplyRequestedCursor()
{
var requested = ImGui.GetMouseCursor();
if (requested == _lastRequestedCursor)
return;
_lastRequestedCursor = requested;
// Outside our client area the cursor belongs to whoever is under it
if (!ClientRectangle.Contains(PointToClient(Cursor.Position)))
return;
UpdateMouseCursor();
}
private ImGuiMouseCursor _lastRequestedCursor = ImGuiMouseCursor.Arrow;
private bool UpdateMouseCursor()
{
ImGuiIOPtr io = ImGui.GetIO();
if (((uint) io.ConfigFlags & (uint) ImGuiConfigFlags.NoMouseCursorChange) > 0)
return false;
ImGuiMouseCursor imgui_cursor = ImGui.GetMouseCursor();
if (imgui_cursor == ImGuiMouseCursor.None || io.MouseDrawCursor)
{
// Hide OS mouse cursor if imgui is drawing it or if it wants no cursor
Cursor.Current = null;
return true;
}
Cursor newCursor;
// Show OS mouse cursor
switch (imgui_cursor)
{
case ImGuiMouseCursor.Arrow:
newCursor = Cursors.Arrow;
break;
case ImGuiMouseCursor.TextInput:
newCursor = Cursors.IBeam;
break;
case ImGuiMouseCursor.ResizeAll:
newCursor = Cursors.SizeAll;
break;
case ImGuiMouseCursor.ResizeEW:
newCursor = Cursors.SizeWE;
break;
case ImGuiMouseCursor.ResizeNS:
newCursor = Cursors.SizeNS;
break;
case ImGuiMouseCursor.ResizeNESW:
newCursor = Cursors.SizeNESW;
break;
case ImGuiMouseCursor.ResizeNWSE:
newCursor = Cursors.SizeNWSE;
break;
case ImGuiMouseCursor.Hand:
newCursor = Cursors.Hand;
break;
default:
newCursor = Cursors.Arrow;
break;
}
if (Cursor.Current != newCursor)
{
Cursor = newCursor;
// Apply immediately - the next WM_SETCURSOR only comes with the next mouse move
Cursor.Current = newCursor;
}
return true;
}
}