using NativeEngine; using Sandbox.Internal; using Sandbox.UI; namespace Sandbox.Engine; /// /// This is where input is sent to from the engine. This is the first place input is routed to. /// From here it tries to route it to the menu, game menu and client - in that order. That should /// really be abstracted out though, so we can use this properly in Standalone. /// internal static partial class InputRouter { /// /// True if the cursor is visible /// public static bool MouseCursorVisible { get; private set; } /// /// The mouse cursor position. Or the last position if it's now invisible. /// public static Vector2 MouseCursorPosition { get; private set; } /// /// The mouse cursor delta /// public static Vector2 MouseCursorDelta { get; private set; } /// /// The panel we're keyboard focusing on /// public static IPanel KeyboardFocusPanel { get; set; } /// /// The position in which we entered capture/relative mode /// static Vector2? mouseCapturePosition; /// /// True if an "exit game" button is pressed, escape on keyboard /// public static bool EscapeIsDown { get; private set; } /// /// The escape button was pressed this frame. /// The game is allowed to consume this. Then it will go to the menu. /// This is distinct from EscapeIsDown, because that is used to close the game when held down. /// public static bool EscapeWasPressed { get; set; } /// /// Time since escape was pressed /// static RealTimeSince TimeSinceEscapePressed { get; set; } /// /// Buttons that are currently pressed /// static HashSet PressedButtons = new HashSet(); /// /// Controller buttons that are currently pressed /// static HashSet PressedControllerButtons = new HashSet(); /// /// Returns the number of seconds escape has been held down /// public static float EscapeTime => EscapeIsDown ? TimeSinceEscapePressed.Relative : 0; /// /// Return the input contexts of each context, in order of priority /// static IEnumerable Contexts { get { if ( IMenuDll.Current is not null ) { var menu = IMenuDll.Current.InputContext; if ( menu is not null ) yield return menu; } // if we even have a game menu! if ( IGameInstance.Current is not null ) { var gamemenu = IGameInstanceDll.Current.InputContext; if ( gamemenu is not null ) yield return gamemenu; } } } public static void Frame() { var activeMouse = Contexts.Where( x => x.MouseState != InputContext.InputState.Ignore ).FirstOrDefault(); var activeKeyboard = Contexts.Where( x => x.KeyboardState != InputContext.InputState.Ignore ).FirstOrDefault(); // Capture mode could either come from being in game (in which case input is sent to the game) // or from a Panel.CaptureMode - in which case input is sent to the panel/ui bool mouseCaptureMode = activeMouse is not null && activeMouse.MouseState == InputContext.InputState.Game; mouseCaptureMode = mouseCaptureMode || (activeMouse?.MouseCapture ?? false); MouseCursorVisible = !mouseCaptureMode && (activeMouse is not null && activeMouse.MouseState == InputContext.InputState.UI); if ( !InputSystem.HasMouseFocus() ) MouseCursorVisible = true; if ( mouseCaptureMode ) { // save the cursor position if ( mouseCapturePosition is null ) { mouseCapturePosition = MouseCursorPosition; } NativeEngine.InputSystem.SetRelativeMouseMode( true ); } else { NativeEngine.InputSystem.SetRelativeMouseMode( false ); // restore cursor position if ( mouseCapturePosition is not null ) { SetCursorPosition( mouseCapturePosition.Value ); mouseCapturePosition = null; } } if ( activeMouse is not null ) { SetCursorType( activeMouse.MouseCursor ); } if ( activeKeyboard is not null ) { KeyboardFocusPanel = activeKeyboard.KeyboardFocusPanel; } if ( KeyboardFocusPanel is null ) { NativeEngine.InputSystem.SetIMEAllowed( false ); } else { NativeEngine.InputSystem.SetIMEAllowed( true ); var rect = KeyboardFocusPanel.Rect; NativeEngine.InputSystem.SetIMETextLocation( (int)rect.Left, (int)rect.Top, (int)rect.Width, (int)rect.Height ); } MouseCursorDelta = 0; EscapeWasPressed = false; TooltipSystem.SetHovered( activeMouse?.MouseFocusPanel ?? null ); } static void SetCursorPosition( Vector2 pos ) { if ( !g_pInputService.IsAppActive() ) return; if ( !InputSystem.HasMouseFocus() ) return; g_pInputService.SetCursorPosition( (int)pos.x, (int)pos.y ); } static string CursorName { get; set; } static readonly CaseInsensitiveDictionary CursorLookup = new() { { "none", InputStandardCursor_t.None }, { "arrow", InputStandardCursor_t.Arrow }, { "ibeam", InputStandardCursor_t.IBeam }, { "text", InputStandardCursor_t.IBeam }, { "crosshair", InputStandardCursor_t.Crosshair }, { "pointer", InputStandardCursor_t.Hand }, { "hand", InputStandardCursor_t.Hand }, { "progress", InputStandardCursor_t.WaitArrow }, { "wait", InputStandardCursor_t.HourGlass }, { "hourglass", InputStandardCursor_t.HourGlass }, { "move", InputStandardCursor_t.SizeALL }, { "sizenesw", InputStandardCursor_t.SizeNESW }, { "nesw-resize", InputStandardCursor_t.SizeNESW }, { "sizenwse", InputStandardCursor_t.SizeNWSE }, { "nwse-resize", InputStandardCursor_t.SizeNWSE }, { "sizewe", InputStandardCursor_t.SizeWE }, { "ew-resize", InputStandardCursor_t.SizeWE }, { "sizens", InputStandardCursor_t.SizeNS }, { "ns-resize", InputStandardCursor_t.SizeNS }, { "not-allowed", InputStandardCursor_t.No }, }; static readonly HashSet UserCursors = new(); static void SetCursorType( string name ) { name = MouseCursorVisible ? string.IsNullOrWhiteSpace( name ) ? "arrow" : name.ToLower() : "none"; if ( name == CursorName ) return; if ( name == "none" ) { InputSystem.SetCursorStandard( InputStandardCursor_t.None ); } else if ( UserCursors.Contains( name ) ) { InputSystem.SetCursorUser( name ); } else if ( CursorLookup.TryGetValue( name, out var found ) ) { InputSystem.SetCursorStandard( found ); } else { name = "arrow"; if ( name == CursorName ) return; InputSystem.SetCursorStandard( InputStandardCursor_t.Arrow ); } CursorName = name; } internal static void ShutdownUserCursors() { if ( Application.IsHeadless ) return; UserCursors.Clear(); InputSystem.ShutdownUserCursors(); } internal static void CreateUserCursor( BaseFileSystem filesystem, string name, string filepath, int hotX, int hotY ) { Assert.False( Application.IsHeadless ); if ( string.IsNullOrWhiteSpace( name ) ) return; if ( string.IsNullOrWhiteSpace( filepath ) ) return; if ( UserCursors.Contains( name ) ) return; if ( !filesystem.FileExists( filepath ) ) return; if ( !InputSystem.LoadCursorFromFile( filepath, name, hotX, hotY ) ) return; UserCursors.Add( name.ToLower() ); } /// /// An input context wants to set the cursor position /// internal static void SetCursorPosition( InputContext inputContext, Vector2 vector2 ) { var activeMouse = Contexts.Where( x => x.MouseState != InputContext.InputState.Ignore ) .FirstOrDefault(); if ( activeMouse != inputContext ) return; // if this is set, we're in capture mode - so just update the position // which will update the position of the cursor when we come out of it if ( mouseCapturePosition is not null ) { mouseCapturePosition = vector2; return; } SetCursorPosition( vector2 ); } /// /// Return true if button is pressed /// public static bool IsButtonDown( ButtonCode code ) { return PressedButtons.Contains( code ); } /// /// Return true if button is pressed /// private static void SetButtonState( ButtonCode code, bool state ) { if ( state ) PressedButtons.Add( code ); else PressedButtons.Remove( code ); } /// /// Return true if button is pressed /// public static bool IsButtonDown( GamepadCode code ) { return PressedControllerButtons.Contains( code ); } /// /// Return true if button is pressed /// private static void SetButtonState( GamepadCode code, bool state ) { if ( state ) PressedControllerButtons.Add( code ); else PressedControllerButtons.Remove( code ); } /// /// A console command from the engine. /// internal static void OnConsoleCommand( string v ) { ConVarSystem.Run( v ); } internal static void CloseApplication() { Application.Exit(); } }