using NativeEngine; using Sandbox.Internal; using Sandbox.UI; namespace Sandbox.Engine; internal sealed class InputContext { /// /// The name of this context, for debugging purposes /// public string Name { get; set; } /// /// What mouse cursor does this context want to show? /// public string MouseCursor { get; set; } /// /// What kind of mouse interaction is this context interested in right now /// public InputState MouseState { get; private set; } /// /// Mouse is UI mode but wants to use the mouse capture/delta mode /// public bool MouseCapture { get; private set; } /// /// What kind of keyboard interaction is this context interested in right now /// public InputState KeyboardState { get; private set; } HashSet _pressed = []; /// /// When input type changes, we capture which buttons are down to block text input until they're released /// This fixes problems where you hold a key, open a UI, and the held key generates text input /// List _blockingTextInput = []; public Action OnGameMouseWheel { get; set; } /// /// Mouse moved in game mode /// public Action OnMouseMotion { get; set; } /// /// A button event to be sent to the game /// public Action OnGameButton { get; set; } public IPanel KeyboardFocusPanel { get; internal set; } public IPanel MouseFocusPanel { get; internal set; } /// /// Which system should we be sending our input to? /// public UISystem TargetUISystem { get; set; } static bool IsMouseButton( ButtonCode btton ) { if ( btton < ButtonCode.MOUSE_FIRST ) return false; if ( btton > ButtonCode.MOUSE_LAST ) return false; return true; } /// /// When true we've called StartTrapping and are waiting for the user to release keys /// bool TrappingKeys { get; set; } HashSet TrappedKeys { get; set; } static Action TrapCallback; /// /// Start trapping keys. When the user releases all keys the callback will be called /// with a list of buttons that were pressed during the trap. /// public void StartTrapping( Action callback ) { TrappingKeys = true; TrappedKeys = new HashSet(); TrapCallback = callback; } /// /// Called when a key is released if we're trapping keys. /// void EndTrapping() { TrappingKeys = false; TrapCallback?.Invoke( TrappedKeys.ToArray() ); TrapCallback = null; } internal void IN_Text( char input ) { if ( TrappingKeys ) return; // A button is being held that was down when we switched to UI mode // so we just block text input until it's released if ( _blockingTextInput.Any() ) { //Log.Info( $"In_Text blocked [{input}] {string.Join( ",", _blockingTextInput )}" ); return; } TargetUISystem.InputEventQueue.AddKeyTyped( (char)input ); } internal void IN_MouseWheel( Vector2 value, KeyboardModifiers modifiers ) { if ( MouseFocusPanel is not null && MouseFocusPanel.WantsPointerEvents ) { TargetUISystem.Input.AddMouseWheel( value, modifiers ); } else { OnGameMouseWheel?.Invoke( value ); } } public void BlockTextInputUntilButtonsReleased() { _blockingTextInput.Clear(); _blockingTextInput.AddRange( _pressed.Where( x => !IsMouseButton( x ) ) ); // Log.Info( $"BLOCKING TEXT UNTIL UP {string.Join( ",", _blockingTextInput )}" ); } internal void IN_ImeStart() { TargetUISystem.CurrentFocus?.CreateEvent( "onimestart" ); } internal void IN_ImeEnd() { TargetUISystem.CurrentFocus?.CreateEvent( "onimeend" ); } internal void IN_ImeComposition( string text, bool final ) { TargetUISystem.CurrentFocus?.CreateEvent( "onime", text ); } internal void In_MousePosition( Vector2 pos, Vector2 delta ) { if ( delta.Length == 0 ) return; if ( MouseState == InputState.Game || MouseCapture ) { OnMouseMotion?.Invoke( delta ); return; } TargetUISystem.InputEventQueue.MouseMoved( delta ); } /// /// Special handling for the escape button. Return false if we didn't use it. /// internal bool In_Escape() { if ( TrappingKeys ) { EndTrapping(); return true; } if ( KeyboardState == InputState.Game ) { TargetUISystem.CurrentFocus?.CreateEvent( "onescape" ); return true; } if ( KeyboardFocusPanel is null ) return false; if ( KeyboardFocusPanel.Parent is null ) return false; TargetUISystem.CurrentFocus?.CreateEvent( "onescape" ); return true; } internal void ReleaseAllButtons() { foreach ( var button in _pressed.ToArray() ) { IN_ButtonReleased( button, KeyboardModifiers.None ); } } /// /// This is called even if the context doesn't have focus. /// It's just a place to unpress buttons, if they're down. /// internal void IN_ButtonReleased( ButtonCode button, KeyboardModifiers modifiers ) { if ( TrappingKeys ) return; _blockingTextInput.Remove( button ); if ( !_pressed.Contains( button ) ) return; _pressed.Remove( button ); TargetUISystem.InputEventQueue.AddButtonEvent( button, false, modifiers ); var name = InputSystem.CodeToString( button ); if ( !string.IsNullOrWhiteSpace( name ) ) { OnGameButton?.Invoke( button, name, false ); } } internal void IN_Button( bool pressed, ButtonCode button, bool repeat, KeyboardModifiers modifiers ) { if ( TrappingKeys ) { var name = InputSystem.CodeToString( button ); if ( !string.IsNullOrWhiteSpace( name ) ) { TrappedKeys.Add( name ); } if ( !pressed ) { EndTrapping(); } return; } if ( IsMouseButton( button ) ) { OnMouseButton( button, pressed, modifiers ); return; } if ( pressed && KeyboardState == InputState.UI ) { TargetUISystem.InputEventQueue.AddButtonTyped( button, modifiers ); } // We reserve some buttons that we handle ourselves, like function keys and ESCAPE. if ( IsReservedButton( button ) ) return; OnButton( button, pressed, repeat, modifiers ); } // cleanme internal Vector2 lastClickPos; internal RealTimeSince timeSinceClick; internal int clickCounter; void OnMouseButton( ButtonCode button, bool pressed, KeyboardModifiers modifiers ) { var gameToo = MouseFocusPanel?.ButtonInput == PanelInputType.Game; if ( MouseFocusPanel is null || !MouseFocusPanel.WantsPointerEvents ) gameToo = true; if ( MouseState == InputState.Ignore ) return; if ( MouseState == InputState.Game || gameToo || !pressed ) { var name = InputSystem.CodeToString( button ); if ( !string.IsNullOrWhiteSpace( name ) ) { OnGameButton?.Invoke( button, name, pressed ); } } // Slightly dodgy double/triple click handling // lets hope no-one notices you can click with left and then right to double click if ( button != ButtonCode.MouseWheelDown && button != ButtonCode.MouseWheelUp ) { var clickDelta = lastClickPos - InputRouter.MouseCursorPosition; lastClickPos = InputRouter.MouseCursorPosition; if ( timeSinceClick > 0.4f || clickDelta.Length > 5.0f ) { clickCounter = 0; } if ( !pressed ) clickCounter++; timeSinceClick = 0; if ( !pressed && clickCounter == 2 ) { //Log.Info( "Double Click" ); TargetUISystem.InputEventQueue.AddDoubleClick( button.ToString() ); } if ( !pressed && clickCounter == 3 ) { //Log.Info( "Triple Click" ); //OnTripleClick?.Invoke( button.ToString() ); } } if ( button == ButtonCode.MouseWheelDown || button == ButtonCode.MouseWheelUp ) { // OnMouseWheel?.Invoke( button == ButtonCode.MouseWheelDown ? 1 : -1 ); return; } if ( _pressed.Contains( button ) == pressed ) return; if ( pressed ) _pressed.Add( button ); else _pressed.Remove( button ); if ( MouseState == InputState.UI ) { if ( pressed ) { TargetUISystem.InputEventQueue.AddButtonTyped( button, modifiers ); } TargetUISystem.Input.AddMouseButton( button, pressed, modifiers ); } } /// /// Is this a reserved button? This means developers can not detect these keys as up or down. /// /// /// bool IsReservedButton( ButtonCode button ) { return button switch { >= ButtonCode.KEY_F1 and <= ButtonCode.KEY_F12 => true, ButtonCode.KEY_ESCAPE => true, _ => false }; } void OnButton( ButtonCode button, bool down, bool repeat, KeyboardModifiers modifiers ) { // not right now if ( repeat ) return; // equals is on purpose - // we only want this if they don't have shift and alt etc if ( modifiers == KeyboardModifiers.Ctrl && KeyboardState == InputState.UI ) { if ( button == ButtonCode.KEY_C ) { if ( !down ) return; TargetUISystem.InputEventQueue.QueueInputEvent( new CopyEvent() ); return; } if ( button == ButtonCode.KEY_V ) { if ( !down ) return; if ( EngineGlobal.Plat_HasClipboardText() ) { TargetUISystem.InputEventQueue.QueueInputEvent( new PasteEvent( EngineGlobal.Plat_GetClipboardText() ) ); } return; } if ( button == ButtonCode.KEY_X ) { if ( !down ) return; TargetUISystem.InputEventQueue.QueueInputEvent( new CutEvent() ); return; } } // always allow the actions to "release" when UI pops up, // but don't allow new presses if ( KeyboardState == InputState.Game || !down ) { var name = InputSystem.CodeToString( button ); if ( !string.IsNullOrWhiteSpace( name ) ) { OnGameButton?.Invoke( button, name, down ); } } if ( _pressed.Contains( button ) == down ) return; if ( down ) _pressed.Add( button ); else { _pressed.Remove( button ); _blockingTextInput.Remove( button ); } if ( KeyboardState == InputState.UI || !down ) { TargetUISystem.InputEventQueue.AddButtonEvent( button, down, modifiers ); } } internal void UpdateInputFromUI( InputState mouseState, IPanel mouseFocus, bool mouseCapture, InputState keyboardState, IPanel keyboardFocus ) { MouseFocusPanel = mouseFocus; MouseState = mouseState; MouseCapture = mouseCapture; if ( KeyboardFocusPanel != keyboardFocus || KeyboardState != keyboardState ) { BlockTextInputUntilButtonsReleased(); KeyboardFocusPanel = keyboardFocus; KeyboardState = keyboardState; } } internal void SetMousePosition( Vector2 vector2 ) { InputRouter.SetCursorPosition( this, vector2 ); } public enum InputState { /// /// Doesn't want it, pass down to next context /// Ignore, /// /// Interacting with UI /// UI, /// /// Interacting with the game /// Game } }