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
}
}