SboxSuperuser/Engine/Vendor/ImGui/ImGui.Input.cs

ImGui input helper methods for mouse and keyboard state, from a vendored ImGui implementation. Exposes functions to query mouse buttons, position, dragging, clicks, keyboard keys, key chords, and updates per-frame input state.

Reflection
// Vendored from zeljkovranjes/sbox-imgui; see LICENSE and PROVENANCE.md in this folder.
using Sandbox;
#pragma warning disable CS0649 // Upstream reserves fields for optional widgets and features.

namespace SboxSuperuser.Engine.Vendor.ImGui;

internal static partial class ImGui
{
	public static ImGuiIO GetIO() => G.IO;

	#region Mouse
	public static bool IsMouseDown( ImGuiMouseButton button ) => G.IO.MouseDown[(int)button];

	public static bool IsMouseClicked( ImGuiMouseButton button, bool repeat = false )
	{
		var g = G;
		int b = (int)button;
		float t = g.IO.MouseDownDuration[b];
		if ( t == 0.0f )
			return true;
		if ( repeat && t > g.IO.KeyRepeatDelay )
			return CalcTypematicRepeatAmount( t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate ) > 0;
		return false;
	}

	public static bool IsMouseReleased( ImGuiMouseButton button ) => G.IO.MouseReleased[(int)button];
	public static bool IsMouseDoubleClicked( ImGuiMouseButton button ) => G.IO.MouseClickedCount[(int)button] == 2 && G.IO.MouseClicked[(int)button];
	public static int GetMouseClickedCount( ImGuiMouseButton button ) => G.IO.MouseClickedCount[(int)button];

	/// <summary>Is mouse hovering given bounding rect (in screen space), clipped by the current clipping settings.</summary>
	public static bool IsMouseHoveringRect( Vector2 rMin, Vector2 rMax, bool clip = true )
	{
		var g = G;
		var rectClipped = new ImRect( rMin, rMax );
		if ( clip && g.CurrentWindow is not null )
			rectClipped.ClipWith( g.CurrentWindow.ClipRect );

		var rectForTouch = rectClipped;
		rectForTouch.Expand( g.Style.TouchExtraPadding );
		return rectForTouch.Contains( g.IO.MousePos );
	}

	public static bool IsMousePosValid( Vector2? mousePos = null )
	{
		const float MOUSE_INVALID = -256000.0f;
		var p = mousePos ?? G.IO.MousePos;
		return p.x >= MOUSE_INVALID && p.y >= MOUSE_INVALID;
	}

	public static bool IsAnyMouseDown()
	{
		var io = G.IO;
		for ( int n = 0; n < io.MouseDown.Length; n++ )
			if ( io.MouseDown[n] ) return true;
		return false;
	}

	public static Vector2 GetMousePos() => G.IO.MousePos;

	public static Vector2 GetMousePosOnOpeningCurrentPopup()
	{
		var g = G;
		if ( g.BeginPopupStack.Count > 0 )
			return g.OpenPopupStack[g.BeginPopupStack.Count - 1].OpenMousePos;
		return g.IO.MousePos;
	}

	public static bool IsMouseDragging( ImGuiMouseButton button, float lockThreshold = -1.0f )
	{
		var g = G;
		if ( !g.IO.MouseDown[(int)button] )
			return false;
		return IsMouseDragPastThreshold( button, lockThreshold );
	}

	internal static bool IsMouseDragPastThreshold( ImGuiMouseButton button, float lockThreshold = -1.0f )
	{
		var g = G;
		if ( lockThreshold < 0.0f )
			lockThreshold = g.IO.MouseDragThreshold;
		return g.IO.MouseDragMaxDistanceSqr[(int)button] >= lockThreshold * lockThreshold;
	}

	public static Vector2 GetMouseDragDelta( ImGuiMouseButton button = ImGuiMouseButton.Left, float lockThreshold = -1.0f )
	{
		var g = G;
		int b = (int)button;
		if ( lockThreshold < 0.0f )
			lockThreshold = g.IO.MouseDragThreshold;
		if ( g.IO.MouseDown[b] || g.IO.MouseReleased[b] )
			if ( g.IO.MouseDragMaxDistanceSqr[b] >= lockThreshold * lockThreshold )
				if ( IsMousePosValid( g.IO.MousePos ) && IsMousePosValid( g.IO.MouseClickedPos[b] ) )
					return g.IO.MousePos - g.IO.MouseClickedPos[b];
		return Vector2.Zero;
	}

	public static void ResetMouseDragDelta( ImGuiMouseButton button = ImGuiMouseButton.Left )
	{
		var g = G;
		g.IO.MouseClickedPos[(int)button] = g.IO.MousePos;
	}

	public static ImGuiMouseCursor GetMouseCursor() => G.MouseCursor;
	public static void SetMouseCursor( ImGuiMouseCursor cursorType ) => G.MouseCursor = cursorType;

	public static void SetNextFrameWantCaptureMouse( bool wantCaptureMouse ) => G.WantCaptureMouseNextFrame = wantCaptureMouse ? 1 : 0;
	public static void SetNextFrameWantCaptureKeyboard( bool wantCaptureKeyboard ) => G.WantCaptureKeyboardNextFrame = wantCaptureKeyboard ? 1 : 0;
	#endregion

	#region Keyboard
	public static bool IsKeyDown( ImGuiKey key )
	{
		if ( TryModKey( key, out var down ) ) return down;
		int idx = (int)key;
		if ( idx <= 0 || idx >= (int)ImGuiKey.COUNT ) return false;
		return G.IO.KeysData[idx].Down;
	}

	public static bool IsKeyPressed( ImGuiKey key, bool repeat = true )
	{
		int idx = (int)key;
		if ( idx <= 0 || idx >= (int)ImGuiKey.COUNT ) return false;
		var io = G.IO;
		var data = io.KeysData[idx];
		float t = data.DownDuration;
		if ( t < 0.0f ) return false;
		if ( t == 0.0f ) return true;
		if ( repeat && t > io.KeyRepeatDelay )
			return CalcTypematicRepeatAmount( t - io.DeltaTime, t, io.KeyRepeatDelay, io.KeyRepeatRate ) > 0;
		return false;
	}

	public static bool IsKeyReleased( ImGuiKey key )
	{
		int idx = (int)key;
		if ( idx <= 0 || idx >= (int)ImGuiKey.COUNT ) return false;
		var data = G.IO.KeysData[idx];
		return data.DownDurationPrev >= 0.0f && !data.Down;
	}

	public static bool IsKeyChordPressed( ImGuiKey keyChord )
	{
		var io = G.IO;
		var mods = (ImGuiKey)((int)keyChord & (int)ImGuiKey.ImGuiMod_Mask_);
		var key = (ImGuiKey)((int)keyChord & ~(int)ImGuiKey.ImGuiMod_Mask_);
		if ( ((mods & ImGuiKey.ImGuiMod_Ctrl) != 0) != io.KeyCtrl ) return false;
		if ( ((mods & ImGuiKey.ImGuiMod_Shift) != 0) != io.KeyShift ) return false;
		if ( ((mods & ImGuiKey.ImGuiMod_Alt) != 0) != io.KeyAlt ) return false;
		return key == ImGuiKey.None || IsKeyPressed( key, false );
	}

	public static bool Shortcut( ImGuiKey keyChord ) => IsKeyChordPressed( keyChord );

	public static int GetKeyPressedAmount( ImGuiKey key, float repeatDelay, float rate )
	{
		int idx = (int)key;
		if ( idx <= 0 || idx >= (int)ImGuiKey.COUNT ) return 0;
		var data = G.IO.KeysData[idx];
		if ( !data.Down ) return 0;
		float t = data.DownDuration;
		return CalcTypematicRepeatAmount( t - G.IO.DeltaTime, t, repeatDelay, rate );
	}

	public static string GetKeyName( ImGuiKey key )
	{
		if ( key == ImGuiKey.None ) return "None";
		var name = key.ToString();
		return name.StartsWith( '_' ) ? name.Substring( 1 ) : name;
	}

	private static bool TryModKey( ImGuiKey key, out bool down )
	{
		var io = G.IO;
		switch ( key )
		{
			case ImGuiKey.ImGuiMod_Ctrl: down = io.KeyCtrl; return true;
			case ImGuiKey.ImGuiMod_Shift: down = io.KeyShift; return true;
			case ImGuiKey.ImGuiMod_Alt: down = io.KeyAlt; return true;
			case ImGuiKey.ImGuiMod_Super: down = io.KeySuper; return true;
		}
		down = false;
		return false;
	}

	internal static int CalcTypematicRepeatAmount( float t0, float t1, float repeatDelay, float repeatRate )
	{
		if ( t1 == 0.0f )
			return 1;
		if ( t0 >= t1 )
			return 0;
		if ( repeatRate <= 0.0f )
			return (t0 < repeatDelay && t1 >= repeatDelay) ? 1 : 0;
		int countT0 = (t0 < repeatDelay) ? -1 : (int)((t0 - repeatDelay) / repeatRate);
		int countT1 = (t1 < repeatDelay) ? -1 : (int)((t1 - repeatDelay) / repeatRate);
		return countT1 - countT0;
	}

	/// <summary>Focus the next (or offset-th next) text input widget so it receives keyboard input.</summary>
	public static void SetKeyboardFocusHere( int offset = 0 )
	{
		var g = G;
		g.FocusRequestWindow = g.CurrentWindow;
		g.FocusRequestCounter = offset;
		g.FocusItemCounter = 0;
	}
	#endregion

	#region Frame input update
	internal static void UpdateMouseInputs()
	{
		var g = G;
		var io = g.IO;

		if ( IsMousePosValid( io.MousePos ) )
			io.MousePos = g.MouseLastValidPos = ImFloor( io.MousePos );

		if ( IsMousePosValid( io.MousePos ) && IsMousePosValid( io.MousePosPrev ) )
			io.MouseDelta = io.MousePos - io.MousePosPrev;
		else
			io.MouseDelta = Vector2.Zero;

		const float mouseStationaryThreshold = 2.0f;
		bool mouseStationary = ImLengthSqr( io.MouseDelta ) <= mouseStationaryThreshold * mouseStationaryThreshold;
		g.MouseStationaryTimer = mouseStationary ? g.MouseStationaryTimer + io.DeltaTime : 0.0f;

		io.MousePosPrev = io.MousePos;
		for ( int i = 0; i < io.MouseDown.Length; i++ )
		{
			io.MouseClicked[i] = io.MouseDown[i] && io.MouseDownDuration[i] < 0.0f;
			io.MouseClickedCount[i] = 0;
			io.MouseReleased[i] = !io.MouseDown[i] && io.MouseDownDuration[i] >= 0.0f;
			io.MouseDownDurationPrev[i] = io.MouseDownDuration[i];
			io.MouseDownDuration[i] = io.MouseDown[i] ? (io.MouseDownDuration[i] < 0.0f ? 0.0f : io.MouseDownDuration[i] + io.DeltaTime) : -1.0f;
			if ( io.MouseClicked[i] )
			{
				bool isRepeatedClick = false;
				if ( (float)(g.Time - io.MouseClickedTime[i]) < io.MouseDoubleClickTime )
				{
					var deltaFromClickPos = IsMousePosValid( io.MousePos ) ? (io.MousePos - io.MouseClickedPos[i]) : Vector2.Zero;
					if ( ImLengthSqr( deltaFromClickPos ) < io.MouseDoubleClickMaxDist * io.MouseDoubleClickMaxDist )
						isRepeatedClick = true;
				}
				if ( isRepeatedClick )
					io.MouseClickedLastCount[i]++;
				else
					io.MouseClickedLastCount[i] = 1;
				io.MouseClickedTime[i] = g.Time;
				io.MouseClickedPos[i] = io.MousePos;
				io.MouseClickedCount[i] = io.MouseClickedLastCount[i];
				io.MouseDragMaxDistanceSqr[i] = 0.0f;
			}
			else if ( io.MouseDown[i] )
			{
				var deltaFromClickPos = IsMousePosValid( io.MousePos ) ? (io.MousePos - io.MouseClickedPos[i]) : Vector2.Zero;
				io.MouseDragMaxDistanceSqr[i] = MathF.Max( io.MouseDragMaxDistanceSqr[i], ImLengthSqr( deltaFromClickPos ) );
			}
			io.MouseDoubleClicked[i] = io.MouseClickedCount[i] == 2;
		}
	}

	internal static void UpdateKeyboardInputs()
	{
		var g = G;
		var io = g.IO;
		for ( int k = (int)ImGuiKey.Tab; k < (int)ImGuiKey.COUNT; k++ )
		{
			var key = (ImGuiKey)k;
			bool down = io.KeysDownFromEvents[k] || ImGuiHost.PollKeyDown( key );
			ref var data = ref io.KeysData[k];
			data.Down = down;
			data.DownDurationPrev = data.DownDuration;
			data.DownDuration = down ? (data.DownDuration < 0.0f ? 0.0f : data.DownDuration + io.DeltaTime) : -1.0f;
		}

		io.KeyCtrl = io.KeysData[(int)ImGuiKey.LeftCtrl].Down || io.KeysData[(int)ImGuiKey.RightCtrl].Down;
		io.KeyShift = io.KeysData[(int)ImGuiKey.LeftShift].Down || io.KeysData[(int)ImGuiKey.RightShift].Down;
		io.KeyAlt = io.KeysData[(int)ImGuiKey.LeftAlt].Down || io.KeysData[(int)ImGuiKey.RightAlt].Down;
		io.KeySuper = io.KeysData[(int)ImGuiKey.LeftSuper].Down || io.KeysData[(int)ImGuiKey.RightSuper].Down;
	}
	#endregion
}