Files
sbox-public/engine/Sandbox.Engine/Systems/UI/Input/PanelInput.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

453 lines
12 KiB
C#

using NativeEngine;
using Sandbox.Engine;
using System.Runtime.InteropServices;
namespace Sandbox.UI;
internal class PanelInput
{
/// <summary>
/// Panel we're currently hovered over
/// </summary>
public Panel Hovered { get; private set; }
/// <summary>
/// Panel we're currently pressing down
/// </summary>
public Panel Active { get; private set; }
//public string LastCursor;
public Selection Selection = new Selection();
public PanelInput()
{
MouseStates = new MouseButtonState[5];
for ( int i = 0; i < 5; i++ )
{
MouseStates[i] = new MouseButtonState( this, ButtonCode.MouseLeft + i );
}
}
internal virtual void Tick( IEnumerable<RootPanel> panels, bool mouseIsActive )
{
bool hoveredAny = false;
// When we're ticking inputs, let's emulate the mouse if we're using a gamepad
if ( Input.EnableVirtualCursor && Input.CurrentController is { } controller )
{
var moveX = controller.GetAxis( NativeEngine.GameControllerAxis.LeftX );
var moveY = controller.GetAxis( NativeEngine.GameControllerAxis.LeftY );
if ( MathF.Abs( moveX ) > 0 || MathF.Abs( moveY ) > 0 )
{
var screen = Screen.Size;
var min = MathF.Min( screen.x, screen.y );
Mouse.Position += new Vector2( moveX * min, moveY * min ) * Preferences.ControllerAnalogSpeed * RealTime.Delta;
}
}
var inputData = GetInputData();
if ( mouseIsActive )
{
foreach ( var panel in panels )
{
if ( UpdateMouse( panel, inputData ) )
{
hoveredAny = true;
break;
}
}
}
if ( !hoveredAny )
{
SetHovered( null );
}
}
HashSet<ButtonCode> mousebuttons = new HashSet<ButtonCode>();
Vector2 mouseWheelValue { get; set; }
/// <summary>
/// Called from input when mouse wheel changes
/// </summary>
public void AddMouseWheel( Vector2 value, KeyboardModifiers modifiers )
{
//
// Windows apps will typically translate vertical mouse wheel movement into
// horizontal mouse wheel movement if the shift key is held down during a mouse
// wheel event
// This is also inverted, i.e. scrolling down will scroll to the right
//
if ( modifiers.Contains( KeyboardModifiers.Shift ) )
value = value.WithX( -value.y ).WithY( 0 );
mouseWheelValue -= value;
}
/// <summary>
/// Called from input when mouse wheel changes
/// </summary>
internal void AddMouseButton( ButtonCode code, bool down, KeyboardModifiers modifiers )
{
if ( down ) mousebuttons.Add( code );
else mousebuttons.Remove( code );
}
internal virtual InputData GetInputData()
{
var mouseWheel = mouseWheelValue;
var leftMouseDown = mousebuttons.Contains( ButtonCode.MouseLeft );
// When using a controller, simulate left mouse click, and analog scroll wheel
if ( Input.EnableVirtualCursor && Input.CurrentController is { } controller )
{
leftMouseDown |= InputRouter.IsButtonDown( GamepadCode.A );
const float scrollScale = 0.5f;
var mouseWheelY = controller.GetAxis( GameControllerAxis.RightY, 0 ) * scrollScale;
var mouseWheelX = controller.GetAxis( GameControllerAxis.RightX, 0 ) * scrollScale;
if ( MathF.Abs( mouseWheelX ) > 0f ) mouseWheel.x = mouseWheelX;
if ( MathF.Abs( mouseWheelY ) > 0f ) mouseWheel.y = mouseWheelY;
}
var d = new InputData();
d.MousePos = Mouse.Position;
d.Mouse0 = leftMouseDown;
d.Mouse1 = mousebuttons.Contains( ButtonCode.MouseMiddle );
d.Mouse2 = mousebuttons.Contains( ButtonCode.MouseRight );
d.Mouse3 = mousebuttons.Contains( ButtonCode.MouseBack );
d.Mouse4 = mousebuttons.Contains( ButtonCode.MouseForward );
d.MouseWheel = mouseWheel;
mouseWheelValue = 0;
return d;
}
/// <summary>
/// The cursor should change. Name could be null, meaning default.
/// </summary>
public virtual void SetCursor( string name ) => Mouse.CursorType = name;
internal virtual bool UpdateMouse( RootPanel root, InputData data )
{
root.MousePos = data.MousePos;
if ( !UpdateHovered( root, data.MousePos ) )
return false;
var leftMousePressed = !MouseStates[0].Pressed && data.Mouse0;
var leftMouseReleased = MouseStates[0].Pressed && !data.Mouse0;
MouseStates[0].Update( data.Mouse0, Hovered );
MouseStates[1].Update( data.Mouse2, Hovered );
MouseStates[2].Update( data.Mouse1, Hovered );
MouseStates[3].Update( data.Mouse3, Hovered );
MouseStates[4].Update( data.Mouse4, Hovered );
Active = null;
if ( MouseStates[2].Active != null ) Active = MouseStates[2].Active;
if ( MouseStates[1].Active != null ) Active = MouseStates[1].Active;
if ( MouseStates[0].Active != null ) Active = MouseStates[0].Active;
if ( Hovered != null )
{
if ( data.MouseWheel != Vector2.Zero )
{
Hovered.OnMouseWheel( data.MouseWheel );
}
}
Selection.UpdateSelection( root, Hovered, data.Mouse0, leftMousePressed, leftMouseReleased, data.MousePos );
return true;
}
bool UpdateHovered( Panel panel, Vector2 pos )
{
Panel current = null;
if ( !CheckHover( panel, pos, ref current ) )
{
return false;
}
if ( MouseStates[0].Dragged )
return true;
SetHovered( current );
return true;
}
internal void SetHovered( Panel current )
{
if ( current != Hovered )
{
if ( Hovered != null )
{
Panel.Switch( PseudoClass.Hover, false, Hovered, current );
Hovered.CreateEvent( new MousePanelEvent( "onmouseout", Hovered, "none" ) );
}
Hovered = current;
if ( Hovered != null )
{
if ( Active == null || Active == Hovered )
Panel.Switch( PseudoClass.Hover, true, Hovered );
Hovered.CreateEvent( new MousePanelEvent( "onmouseover", Hovered, "none" ) );
}
}
if ( Hovered != null )
{
var cursor = Hovered.ComputedStyle?.Cursor;
SetCursor( cursor );
}
}
bool CheckHover( Panel panel, Vector2 pos, ref Panel current )
{
bool found = false;
if ( !panel.IsVisible )
return false;
if ( panel.ComputedStyle == null )
return false;
//
// Transform using this panel's local matrix
//
pos = panel.GetTransformPosition( pos );
var inside = panel.IsInside( pos );
if ( inside && panel.ComputedStyle.PointerEvents != PointerEvents.None )
{
current = panel;
found = true;
}
//
// If we're outside and this panel has overflow hidden we can avoid testing against the children
//
if ( !inside && (panel.ComputedStyle?.Overflow ?? OverflowMode.Visible) != OverflowMode.Visible )
{
return found;
}
//
// No children
//
if ( panel._renderChildren is null || panel._renderChildren.Count == 0 )
{
return found;
}
int topIndex = -10000;
foreach ( var child in CollectionsMarshal.AsSpan( panel._renderChildren ) )
{
var index = child.GetRenderOrderIndex();
if ( index < topIndex ) continue;
if ( CheckHover( child, pos, ref current ) )
{
topIndex = index;
found = true;
}
}
return found;
}
internal class MouseButtonState
{
public PanelInput Input { get; init; }
public ButtonCode MouseButton { get; init; }
public bool Pressed;
public Panel Active;
public bool Dragged;
MousePanelEvent MouseDownEvent;
/// <summary>
/// Then panel that is potentially being dragged
/// </summary>
public Panel DragTarget;
/// <summary>
/// The point where we first pressed on the Active element
/// </summary>
public Vector2 StartHoldOffsetLocal;
public Vector2 StartHoldOffsetScreen;
public MouseButtonState( PanelInput input, ButtonCode i )
{
Input = input;
MouseButton = i;
}
public void Update( bool down, Panel hovered )
{
var mouseMoved = !Mouse.Delta.IsNearZeroLength;
//
// Watch drag - we might have started dragging
//
if ( Pressed && down && DragTarget != null && mouseMoved && MouseDownEvent.Propagate )
{
var delta = StartHoldOffsetLocal - (DragTarget.MousePosition + DragTarget.ScrollOffset);
if ( delta.Length > 5.0f && !Dragged )
{
Dragged = true;
DragTarget?.CreateEvent( new DragEvent( "ondragstart", DragTarget, StartHoldOffsetLocal, StartHoldOffsetScreen ) );
// We started dragging - stop active panel being active, no click events
{
Panel.Switch( PseudoClass.Active, false, Active );
Panel.Switch( PseudoClass.Hover, false, Active );
Active.CreateEvent( new MousePanelEvent( "onmouseup", Active, GetMouseButtonName( MouseButton ) ) );
Active.OnButtonEvent( new ButtonEvent( MouseButton, false ) );
Active = null;
}
}
if ( Dragged )
{
DragTarget?.CreateEvent( new DragEvent( "ondrag", DragTarget, StartHoldOffsetLocal, StartHoldOffsetScreen ) { MouseDelta = Mouse.Delta } );
}
}
if ( Pressed == down ) return;
Pressed = down;
if ( down ) OnPressed( hovered );
else OnReleased( hovered );
}
string GetMouseButtonName( ButtonCode bc )
{
if ( bc == ButtonCode.MouseLeft ) return "mouseleft";
if ( bc == ButtonCode.MouseRight ) return "mouseright";
if ( bc == ButtonCode.MouseMiddle ) return "mousemiddle";
if ( bc == ButtonCode.MouseBack ) return "mouseback";
if ( bc == ButtonCode.MouseForward ) return "mouseforward";
return bc.ToString().ToLower();
}
void OnPressed( Panel hovered )
{
if ( MouseButton == ButtonCode.MouseBack )
{
hovered?.CreateEvent( new PanelEvent( "onback", hovered ) );
hovered?.OnButtonEvent( new ButtonEvent( MouseButton, true ) );
return;
}
if ( MouseButton == ButtonCode.MouseForward )
{
hovered?.CreateEvent( new PanelEvent( "onforward", hovered ) );
hovered?.OnButtonEvent( new ButtonEvent( MouseButton, true ) );
return;
}
Active = hovered;
IMenuDll.Current?.ClosePopups( hovered );
IGameInstanceDll.Current?.ClosePopups( hovered );
if ( Active == null )
return;
Panel.Switch( PseudoClass.Active, true, Active );
if ( MouseButton == ButtonCode.MouseLeft || MouseButton == ButtonCode.MouseRight )
{
Dragged = false;
DragTarget = Active.FindDragTarget();
if ( DragTarget != null )
{
StartHoldOffsetLocal = DragTarget.MousePosition + DragTarget.ScrollOffset;
StartHoldOffsetScreen = Mouse.Position;
}
}
Active.Focus();
MouseDownEvent = new MousePanelEvent( "onmousedown", Active, GetMouseButtonName( MouseButton ) );
Active.CreateEvent( MouseDownEvent );
Active.OnButtonEvent( new ButtonEvent( MouseButton, true ) );
}
void OnReleased( Panel hovered )
{
if ( MouseButton == ButtonCode.MouseBack || MouseButton == ButtonCode.MouseForward )
{
hovered?.OnButtonEvent( new ButtonEvent( MouseButton, false ) );
return;
}
bool canClick = hovered == Active && !Dragged;
if ( Dragged && DragTarget != null )
{
DragTarget.CreateEvent( new DragEvent( "ondragend", DragTarget, StartHoldOffsetLocal, StartHoldOffsetScreen ) );
Dragged = default;
DragTarget = default;
StartHoldOffsetLocal = default;
StartHoldOffsetScreen = default;
}
if ( Active == null )
return;
if ( canClick )
{
Active.CreateEvent( new MousePanelEvent( "onmouseup", Active, GetMouseButtonName( MouseButton ) ) );
if ( MouseButton == ButtonCode.MouseLeft )
{
Active.CreateEvent( new MousePanelEvent( "onclick", Active, GetMouseButtonName( MouseButton ) ) );
}
else if ( MouseButton == ButtonCode.MouseMiddle )
{
Active.CreateEvent( new MousePanelEvent( "onmiddleclick", Active, GetMouseButtonName( MouseButton ) ) );
}
else if ( MouseButton == ButtonCode.MouseRight )
{
Active.CreateEvent( new MousePanelEvent( "onrightclick", Active, GetMouseButtonName( MouseButton ) ) );
}
}
else
{
Active.CreateEvent( new MousePanelEvent( "onmouseup", Active, GetMouseButtonName( MouseButton ) ) );
Panel.Switch( PseudoClass.Hover, false, Active, hovered );
}
Panel.Switch( PseudoClass.Active, false, Active );
Active.OnButtonEvent( new ButtonEvent( MouseButton, false ) );
Active = null;
}
}
internal MouseButtonState[] MouseStates;
}