using NativeEngine; using Sandbox.Engine; using System.Runtime.InteropServices; namespace Sandbox.UI; internal class PanelInput { /// /// Panel we're currently hovered over /// public Panel Hovered { get; private set; } /// /// Panel we're currently pressing down /// 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 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 mousebuttons = new HashSet(); Vector2 mouseWheelValue { get; set; } /// /// Called from input when mouse wheel changes /// 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; } /// /// Called from input when mouse wheel changes /// 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; } /// /// The cursor should change. Name could be null, meaning default. /// 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; /// /// Then panel that is potentially being dragged /// public Panel DragTarget; /// /// The point where we first pressed on the Active element /// 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; }