2022-03-10 14:25:41 +01:00
using System ;
2020-03-17 15:04:36 +01:00
using System.Collections.Generic ;
2019-08-28 18:27:17 +02:00
using System.Linq ;
using Microsoft.Xna.Framework ;
using Microsoft.Xna.Framework.Input ;
2019-08-30 18:15:50 +02:00
using Microsoft.Xna.Framework.Input.Touch ;
2019-08-28 16:38:58 +02:00
using MLEM.Input ;
2019-09-09 16:25:07 +02:00
using MLEM.Misc ;
2019-08-28 18:27:17 +02:00
using MLEM.Ui.Elements ;
2020-05-22 17:02:24 +02:00
using MLEM.Ui.Style ;
2019-08-28 16:38:58 +02:00
namespace MLEM.Ui {
2020-05-22 17:02:24 +02:00
/// <summary>
/// UiControls holds and manages all of the controls for a <see cref="UiSystem"/>.
/// UiControls supports keyboard, mouse, gamepad and touch input using an underlying <see cref="InputHandler"/>.
/// </summary>
2019-08-28 16:38:58 +02:00
public class UiControls {
2020-05-22 17:02:24 +02:00
/// <summary>
/// The input handler that is used for querying input
/// </summary>
2019-08-28 18:27:17 +02:00
public readonly InputHandler Input ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// A list of <see cref="Keys"/>, <see cref="Buttons"/> and/or <see cref="MouseButton"/> that act as the buttons on the keyboard which perform the <see cref="Element.OnPressed"/> action.
/// If the <see cref="ModifierKey.Shift"/> is held, these buttons perform <see cref="Element.OnSecondaryPressed"/>.
/// </summary>
2020-06-12 17:09:35 +02:00
public readonly Keybind KeyboardButtons = new Keybind ( ) . Add ( Keys . Space ) . Add ( Keys . Enter ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-12 17:09:35 +02:00
/// AA <see cref="Keybind"/> that acts as the buttons on a gamepad that perform the <see cref="Element.OnPressed"/> action.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-06-20 23:48:02 +02:00
public readonly Keybind GamepadButtons = new Keybind ( Buttons . A ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-12 17:09:35 +02:00
/// A <see cref="Keybind"/> that acts as the buttons on a gamepad that perform the <see cref="Element.OnSecondaryPressed"/> action.
2020-05-22 17:02:24 +02:00
/// </summary>
2021-06-20 23:48:02 +02:00
public readonly Keybind SecondaryGamepadButtons = new Keybind ( Buttons . X ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-12 17:09:35 +02:00
/// A <see cref="Keybind"/> that acts as the buttons that select a <see cref="Element"/> that is above the currently selected element.
2020-05-22 17:02:24 +02:00
/// </summary>
2020-06-12 17:09:35 +02:00
public readonly Keybind UpButtons = new Keybind ( ) . Add ( Buttons . DPadUp ) . Add ( Buttons . LeftThumbstickUp ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-12 17:09:35 +02:00
/// A <see cref="Keybind"/> that acts as the buttons that select a <see cref="Element"/> that is below the currently selected element.
2020-05-22 17:02:24 +02:00
/// </summary>
2020-06-12 17:09:35 +02:00
public readonly Keybind DownButtons = new Keybind ( ) . Add ( Buttons . DPadDown ) . Add ( Buttons . LeftThumbstickDown ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-12 17:09:35 +02:00
/// A <see cref="Keybind"/> that acts as the buttons that select a <see cref="Element"/> that is to the left of the currently selected element.
2020-05-22 17:02:24 +02:00
/// </summary>
2020-06-12 17:09:35 +02:00
public readonly Keybind LeftButtons = new Keybind ( ) . Add ( Buttons . DPadLeft ) . Add ( Buttons . LeftThumbstickLeft ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-06-12 17:09:35 +02:00
/// A <see cref="Keybind"/> that acts as the buttons that select a <see cref="Element"/> that is to the right of the currently selected element.
2020-05-22 17:02:24 +02:00
/// </summary>
2020-06-12 17:09:35 +02:00
public readonly Keybind RightButtons = new Keybind ( ) . Add ( Buttons . DPadRight ) . Add ( Buttons . LeftThumbstickRight ) ;
2020-05-22 17:02:24 +02:00
/// <summary>
2020-07-16 16:04:18 +02:00
/// All <see cref="Keybind"/> instances used by these ui controls.
/// This can be used to easily serialize and deserialize all ui keybinds.
/// </summary>
public readonly Keybind [ ] Keybinds ;
2022-03-14 16:12:51 +01:00
/// <summary>
/// The <see cref="RootElement"/> that is currently active.
/// The active root element is the one with the highest <see cref="RootElement.Priority"/> that whose <see cref="RootElement.CanSelectContent"/> property is true.
/// </summary>
public RootElement ActiveRoot { get ; protected set ; }
/// <summary>
/// The <see cref="Element"/> that the mouse is currently over.
/// </summary>
public Element MousedElement { get ; protected set ; }
/// <summary>
/// The <see cref="Element"/> that is currently touched.
/// </summary>
public Element TouchedElement { get ; protected set ; }
/// <summary>
/// The element that is currently selected.
/// This is the <see cref="RootElement.SelectedElement"/> of the <see cref="ActiveRoot"/>.
/// </summary>
public Element SelectedElement = > this . GetSelectedElement ( this . ActiveRoot ) ;
2020-07-16 16:04:18 +02:00
/// <summary>
2020-05-22 17:02:24 +02:00
/// The zero-based index of the <see cref="GamePad"/> used for gamepad input.
/// If this index is lower than 0, every connected gamepad will trigger input.
/// </summary>
2019-09-09 16:25:07 +02:00
public int GamepadIndex = - 1 ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this to false to disable mouse input for these ui controls.
/// Note that this does not disable mouse input for the underlying <see cref="InputHandler"/>.
/// </summary>
2019-12-04 01:17:16 +01:00
public bool HandleMouse = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this to false to disable keyboard input for these ui controls.
/// Note that this does not disable keyboard input for the underlying <see cref="InputHandler"/>.
/// </summary>
2019-12-04 01:17:16 +01:00
public bool HandleKeyboard = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this to false to disable touch input for these ui controls.
/// Note that this does not disable touch input for the underlying <see cref="InputHandler"/>.
/// </summary>
2019-12-04 01:17:16 +01:00
public bool HandleTouch = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// Set this to false to disable gamepad input for these ui controls.
/// Note that this does not disable gamepad input for the underlying <see cref="InputHandler"/>.
/// </summary>
2019-12-04 01:17:16 +01:00
public bool HandleGamepad = true ;
2020-05-22 17:02:24 +02:00
/// <summary>
/// If this value is true, the ui controls are in automatic navigation mode.
/// This means that the <see cref="UiStyle.SelectionIndicator"/> will be drawn around the <see cref="SelectedElement"/>.
/// </summary>
2022-03-14 16:12:51 +01:00
public bool IsAutoNavMode {
get = > this . isAutoNavMode ;
set {
if ( this . isAutoNavMode ! = value ) {
this . isAutoNavMode = value ;
this . AutoNavModeChanged ? . Invoke ( value ) ;
}
}
}
/// <summary>
/// An event that is raised when <see cref="IsAutoNavMode"/> is changed.
/// This can be used for custom actions like hiding the mouse cursor when automatic navigation is enabled.
/// </summary>
public event Action < bool > AutoNavModeChanged ;
/// <summary>
/// This value ist true if the <see cref="InputHandler"/> was created by this ui controls instance, or if it was passed in.
/// If the input handler was created by this instance, its <see cref="InputHandler.Update()"/> method should be called by us.
/// </summary>
protected readonly bool IsInputOurs ;
/// <summary>
/// The <see cref="UiSystem"/> that this ui controls instance is controlling
/// </summary>
protected readonly UiSystem System ;
private readonly Dictionary < string , Element > selectedElements = new Dictionary < string , Element > ( ) ;
private bool isAutoNavMode ;
2019-09-09 17:12:36 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
/// Creates a new instance of the ui controls.
/// You should rarely have to invoke this manually, since the <see cref="UiSystem"/> handles it.
/// </summary>
/// <param name="system">The ui system to control with these controls</param>
/// <param name="inputHandler">The input handler to use for controlling, or null to create a new one.</param>
2020-02-06 01:51:41 +01:00
public UiControls ( UiSystem system , InputHandler inputHandler = null ) {
this . System = system ;
2021-02-18 18:36:29 +01:00
this . Input = inputHandler ? ? new InputHandler ( system . Game ) ;
2019-09-11 18:44:05 +02:00
this . IsInputOurs = inputHandler = = null ;
2020-07-16 16:04:18 +02:00
this . Keybinds = typeof ( UiControls ) . GetFields ( )
. Where ( f = > f . FieldType = = typeof ( Keybind ) )
. Select ( f = > ( Keybind ) f . GetValue ( this ) ) . ToArray ( ) ;
2019-08-30 18:15:50 +02:00
// enable all required gestures
2019-08-31 19:32:22 +02:00
InputHandler . EnableGestures ( GestureType . Tap , GestureType . Hold ) ;
2019-08-28 18:27:17 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Update this ui controls instance, causing the underlying <see cref="InputHandler"/> to be updated, as well as ui input to be queried.
/// </summary>
2019-09-11 18:44:05 +02:00
public virtual void Update ( ) {
if ( this . IsInputOurs )
2019-08-28 18:27:17 +02:00
this . Input . Update ( ) ;
2022-03-17 20:36:30 +01:00
this . ActiveRoot = this . System . GetRootElements ( ) . FirstOrDefault ( root = > ! root . Element . IsHidden & & root . CanSelectContent ) ;
2019-08-28 18:27:17 +02:00
2019-08-30 18:15:50 +02:00
// MOUSE INPUT
2019-12-04 01:17:16 +01:00
if ( this . HandleMouse ) {
2022-02-06 22:07:33 +01:00
var mousedNow = this . GetElementUnderPos ( this . Input . ViewportMousePosition . ToVector2 ( ) ) ;
2020-06-04 20:52:21 +02:00
this . SetMousedElement ( mousedNow ) ;
2019-09-02 19:55:26 +02:00
2022-04-30 12:14:08 +02:00
if ( this . Input . IsMouseButtonPressedAvailable ( MouseButton . Left ) ) {
2019-12-04 01:17:16 +01:00
this . IsAutoNavMode = false ;
var selectedNow = mousedNow ! = null & & mousedNow . CanBeSelected ? mousedNow : null ;
2020-03-17 15:04:36 +01:00
this . SelectElement ( this . ActiveRoot , selectedNow ) ;
2022-04-30 12:14:08 +02:00
if ( mousedNow ! = null & & mousedNow . CanBePressed ) {
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementPressed ( mousedNow ) ;
2022-04-30 12:14:08 +02:00
this . Input . TryConsumeMouseButtonPressed ( MouseButton . Left ) ;
}
} else if ( this . Input . IsMouseButtonPressedAvailable ( MouseButton . Right ) ) {
2019-12-04 01:17:16 +01:00
this . IsAutoNavMode = false ;
2022-04-30 12:14:08 +02:00
if ( mousedNow ! = null & & mousedNow . CanBePressed ) {
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementSecondaryPressed ( mousedNow ) ;
2022-04-30 12:14:08 +02:00
this . Input . TryConsumeMouseButtonPressed ( MouseButton . Right ) ;
}
2019-12-04 01:17:16 +01:00
}
2019-08-30 18:15:50 +02:00
}
2019-09-02 19:55:26 +02:00
2019-08-30 18:15:50 +02:00
// KEYBOARD INPUT
2019-12-04 01:17:16 +01:00
if ( this . HandleKeyboard ) {
2022-04-30 12:14:08 +02:00
if ( this . KeyboardButtons . IsPressedAvailable ( this . Input , this . GamepadIndex ) ) {
2020-01-15 17:05:28 +01:00
if ( this . SelectedElement ? . Root ! = null & & this . SelectedElement . CanBePressed ) {
2019-12-04 01:17:16 +01:00
if ( this . Input . IsModifierKeyDown ( ModifierKey . Shift ) ) {
// secondary action on element using space or enter
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementSecondaryPressed ( this . SelectedElement ) ;
2019-12-04 01:17:16 +01:00
} else {
// first action on element using space or enter
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementPressed ( this . SelectedElement ) ;
2019-12-04 01:17:16 +01:00
}
2022-04-30 12:14:08 +02:00
this . KeyboardButtons . TryConsumePressed ( this . Input , this . GamepadIndex ) ;
2019-08-28 18:27:17 +02:00
}
2022-04-30 12:26:40 +02:00
} else if ( this . Input . IsKeyPressedAvailable ( Keys . Tab ) ) {
2019-12-04 01:17:16 +01:00
this . IsAutoNavMode = true ;
// tab or shift-tab to next or previous element
var backward = this . Input . IsModifierKeyDown ( ModifierKey . Shift ) ;
var next = this . GetTabNextElement ( backward ) ;
if ( this . SelectedElement ? . Root ! = null )
next = this . SelectedElement . GetTabNextElement ( backward , next ) ;
2022-04-30 12:26:40 +02:00
if ( next ! = this . SelectedElement ) {
this . SelectElement ( this . ActiveRoot , next ) ;
this . Input . TryConsumeKeyPressed ( Keys . Tab ) ;
}
2019-08-28 18:27:17 +02:00
}
}
2019-09-02 19:55:26 +02:00
2019-08-30 18:15:50 +02:00
// TOUCH INPUT
2019-12-04 01:17:16 +01:00
if ( this . HandleTouch ) {
2022-02-06 22:07:33 +01:00
if ( this . Input . GetViewportGesture ( GestureType . Tap , out var tap ) ) {
2019-12-04 01:17:16 +01:00
this . IsAutoNavMode = false ;
var tapped = this . GetElementUnderPos ( tap . Position ) ;
2020-03-17 15:04:36 +01:00
this . SelectElement ( this . ActiveRoot , tapped ) ;
2020-01-15 17:05:28 +01:00
if ( tapped ! = null & & tapped . CanBePressed )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementPressed ( tapped ) ;
2022-02-06 22:07:33 +01:00
} else if ( this . Input . GetViewportGesture ( GestureType . Hold , out var hold ) ) {
2019-12-04 01:17:16 +01:00
this . IsAutoNavMode = false ;
var held = this . GetElementUnderPos ( hold . Position ) ;
2020-03-17 15:04:36 +01:00
this . SelectElement ( this . ActiveRoot , held ) ;
2020-01-15 17:05:28 +01:00
if ( held ! = null & & held . CanBePressed )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementSecondaryPressed ( held ) ;
2022-02-06 22:07:33 +01:00
} else if ( this . Input . ViewportTouchState . Count < = 0 ) {
2020-06-04 20:52:21 +02:00
this . SetTouchedElement ( null ) ;
} else {
2022-02-06 22:07:33 +01:00
foreach ( var location in this . Input . ViewportTouchState ) {
2020-06-04 20:52:21 +02:00
var element = this . GetElementUnderPos ( location . Position ) ;
if ( location . State = = TouchLocationState . Pressed ) {
// start touching an element if we just touched down on it
this . SetTouchedElement ( element ) ;
} else if ( element ! = this . TouchedElement ) {
// if we moved off of the touched element, we stop touching
this . SetTouchedElement ( null ) ;
}
}
2019-12-04 01:17:16 +01:00
}
2019-08-30 18:15:50 +02:00
}
2019-09-09 16:25:07 +02:00
// GAMEPAD INPUT
2019-12-04 01:17:16 +01:00
if ( this . HandleGamepad ) {
2022-04-30 12:14:08 +02:00
if ( this . GamepadButtons . IsPressedAvailable ( this . Input , this . GamepadIndex ) ) {
if ( this . SelectedElement ? . Root ! = null & & this . SelectedElement . CanBePressed ) {
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementPressed ( this . SelectedElement ) ;
2022-04-30 12:14:08 +02:00
this . GamepadButtons . TryConsumePressed ( this . Input , this . GamepadIndex ) ;
}
} else if ( this . SecondaryGamepadButtons . IsPressedAvailable ( this . Input , this . GamepadIndex ) ) {
if ( this . SelectedElement ? . Root ! = null & & this . SelectedElement . CanBePressed ) {
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementSecondaryPressed ( this . SelectedElement ) ;
2022-04-30 12:14:08 +02:00
this . SecondaryGamepadButtons . TryConsumePressed ( this . Input , this . GamepadIndex ) ;
}
} else if ( this . DownButtons . IsPressedAvailable ( this . Input , this . GamepadIndex ) ) {
if ( this . HandleGamepadNextElement ( Direction2 . Down ) )
this . DownButtons . TryConsumePressed ( this . Input , this . GamepadIndex ) ;
} else if ( this . LeftButtons . IsPressedAvailable ( this . Input , this . GamepadIndex ) ) {
if ( this . HandleGamepadNextElement ( Direction2 . Left ) )
this . LeftButtons . TryConsumePressed ( this . Input , this . GamepadIndex ) ;
} else if ( this . RightButtons . IsPressedAvailable ( this . Input , this . GamepadIndex ) ) {
if ( this . HandleGamepadNextElement ( Direction2 . Right ) )
this . RightButtons . TryConsumePressed ( this . Input , this . GamepadIndex ) ;
} else if ( this . UpButtons . IsPressedAvailable ( this . Input , this . GamepadIndex ) ) {
if ( this . HandleGamepadNextElement ( Direction2 . Up ) )
this . UpButtons . TryConsumePressed ( this . Input , this . GamepadIndex ) ;
2019-12-04 01:17:16 +01:00
}
2019-09-09 16:25:07 +02:00
}
2019-08-28 18:27:17 +02:00
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns the <see cref="Element"/> in the underlying <see cref="UiSystem"/> that is currently below the given position.
/// Throughout the ui system, this is used for mouse input querying.
/// </summary>
/// <param name="position">The position to query</param>
/// <returns>The element under the position, or null if there isn't one</returns>
2021-11-22 15:13:08 +01:00
public virtual Element GetElementUnderPos ( Vector2 position ) {
2020-02-06 01:51:41 +01:00
foreach ( var root in this . System . GetRootElements ( ) ) {
2021-11-22 15:13:08 +01:00
var pos = Vector2 . Transform ( position , root . InvTransform ) ;
2019-09-02 19:55:26 +02:00
var moused = root . Element . GetElementUnderPos ( pos ) ;
2019-08-28 18:27:17 +02:00
if ( moused ! = null )
return moused ;
}
return null ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Selects the given element that is a child of the given root element.
/// Optionally, automatic navigation can be forced on, causing the <see cref="UiStyle.SelectionIndicator"/> to be drawn around the element.
/// A simpler version of this method is <see cref="RootElement.SelectElement"/>.
/// </summary>
/// <param name="root">The root element of the <see cref="Element"/></param>
/// <param name="element">The element to select, or null to deselect the selected element.</param>
/// <param name="autoNav">Whether automatic navigation should be forced on</param>
2020-03-17 15:04:36 +01:00
public void SelectElement ( RootElement root , Element element , bool? autoNav = null ) {
2022-03-17 20:36:30 +01:00
if ( root = = null )
2022-03-11 12:29:56 +01:00
return ;
if ( element ! = null & & ! element . CanBeSelected )
2020-03-17 15:04:36 +01:00
return ;
var selected = this . GetSelectedElement ( root ) ;
if ( selected = = element )
return ;
if ( selected ! = null )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementDeselected ( selected ) ;
2020-03-17 15:04:36 +01:00
if ( element ! = null ) {
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementSelected ( element ) ;
2020-03-17 15:04:36 +01:00
this . selectedElements [ root . Name ] = element ;
} else {
this . selectedElements . Remove ( root . Name ) ;
}
2021-06-09 00:27:50 +02:00
this . System . InvokeOnSelectedElementChanged ( element ) ;
2020-03-17 15:04:36 +01:00
if ( autoNav ! = null )
this . IsAutoNavMode = autoNav . Value ;
}
2020-06-04 20:52:21 +02:00
/// <summary>
/// Sets the <see cref="MousedElement"/> to the given value, calling the appropriate events.
/// </summary>
/// <param name="element">The element to set as moused</param>
public void SetMousedElement ( Element element ) {
2022-03-11 12:29:56 +01:00
if ( element ! = null & & ! element . CanBeMoused )
return ;
2020-06-04 20:52:21 +02:00
if ( element ! = this . MousedElement ) {
if ( this . MousedElement ! = null )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementMouseExit ( this . MousedElement ) ;
2020-06-04 20:52:21 +02:00
if ( element ! = null )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementMouseEnter ( element ) ;
2020-06-04 20:52:21 +02:00
this . MousedElement = element ;
2021-06-09 00:27:50 +02:00
this . System . InvokeOnMousedElementChanged ( element ) ;
2020-06-04 20:52:21 +02:00
}
}
/// <summary>
/// Sets the <see cref="TouchedElement"/> to the given value, calling the appropriate events.
/// </summary>
/// <param name="element">The element to set as touched</param>
public void SetTouchedElement ( Element element ) {
2022-03-11 12:29:56 +01:00
if ( element ! = null & & ! element . CanBeMoused )
return ;
2020-06-04 20:52:21 +02:00
if ( element ! = this . TouchedElement ) {
if ( this . TouchedElement ! = null )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementTouchExit ( this . TouchedElement ) ;
2020-06-04 20:52:21 +02:00
if ( element ! = null )
2021-06-09 00:27:50 +02:00
this . System . InvokeOnElementTouchEnter ( element ) ;
2020-06-04 20:52:21 +02:00
this . TouchedElement = element ;
2021-06-09 00:27:50 +02:00
this . System . InvokeOnTouchedElementChanged ( element ) ;
2020-06-04 20:52:21 +02:00
}
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns the selected element for the given root element.
/// A property equivalent to this method is <see cref="RootElement.SelectedElement"/>.
/// </summary>
/// <param name="root">The root element whose selected element to return</param>
/// <returns>The given root's selected element, or null if the root doesn't exist, or if there is no selected element for that root.</returns>
2020-03-17 15:04:36 +01:00
public Element GetSelectedElement ( RootElement root ) {
2022-03-17 20:36:30 +01:00
if ( root = = null )
2020-03-17 19:29:52 +01:00
return null ;
2020-03-17 15:04:36 +01:00
this . selectedElements . TryGetValue ( root . Name , out var element ) ;
return element ;
}
2020-05-22 17:02:24 +02:00
/// <summary>
/// Returns the next element to select when pressing the <see cref="Keys.Tab"/> key during keyboard navigation.
/// If the <c>backward</c> boolean is true, the previous element should be returned instead.
/// </summary>
/// <param name="backward">If we're going backwards (if <see cref="ModifierKey.Shift"/> is held)</param>
/// <returns>The next or previous element to select</returns>
2019-09-11 18:44:05 +02:00
protected virtual Element GetTabNextElement ( bool backward ) {
2019-09-09 17:18:44 +02:00
if ( this . ActiveRoot = = null )
2019-08-28 18:27:17 +02:00
return null ;
2022-04-14 17:54:25 +02:00
var children = this . ActiveRoot . Element . GetChildren ( c = > ! c . IsHidden , true , true ) . Append ( this . ActiveRoot . Element )
// we can't add these checks to GetChildren because it ignores false grandchildren
. Where ( c = > c . CanBeSelected & & c . AutoNavGroup = = this . SelectedElement ? . AutoNavGroup ) ;
2019-09-09 17:18:44 +02:00
if ( this . SelectedElement ? . Root ! = this . ActiveRoot ) {
2022-05-17 16:06:22 +02:00
// if we don't have an element selected in this root, navigate to the first one without a group
var allowed = children . Where ( c = > c . AutoNavGroup = = null ) ;
return backward ? allowed . LastOrDefault ( ) : allowed . FirstOrDefault ( ) ;
2019-08-28 18:27:17 +02:00
} else {
var foundCurr = false ;
Element lastFound = null ;
foreach ( var child in children ) {
if ( child = = this . SelectedElement ) {
// when going backwards, return the last element found before the current one
if ( backward )
return lastFound ;
foundCurr = true ;
} else {
// when going forwards, return the element after the current one
if ( ! backward & & foundCurr )
return child ;
}
lastFound = child ;
}
return null ;
}
}
2019-08-28 16:38:58 +02:00
2020-05-22 17:02:24 +02:00
/// <summary>
2022-03-10 14:25:41 +01:00
/// Returns the next element that should be selected during gamepad navigation, based on the <see cref="Direction2"/> that we're looking for elements in.
2020-05-22 17:02:24 +02:00
/// </summary>
2022-03-10 14:25:41 +01:00
/// <param name="direction">The direction that we're looking for next elements in</param>
2020-05-22 17:02:24 +02:00
/// <returns>The first element found in that area</returns>
2022-03-10 14:25:41 +01:00
protected virtual Element GetGamepadNextElement ( Direction2 direction ) {
2019-09-11 18:44:05 +02:00
if ( this . ActiveRoot = = null )
return null ;
2022-04-14 18:01:30 +02:00
var children = this . ActiveRoot . Element . GetChildren ( c = > ! c . IsHidden , true , true ) . Append ( this . ActiveRoot . Element )
// we can't add these checks to GetChildren because it ignores false grandchildren
. Where ( c = > c . CanBeSelected & & c . AutoNavGroup = = this . SelectedElement ? . AutoNavGroup ) ;
2019-09-11 18:44:05 +02:00
if ( this . SelectedElement ? . Root ! = this . ActiveRoot ) {
2022-05-17 16:06:22 +02:00
// if we don't have an element selected in this root, navigate to the first one without a group
return children . FirstOrDefault ( c = > c . AutoNavGroup = = null ) ;
2019-09-11 18:44:05 +02:00
} else {
Element closest = null ;
2022-03-14 15:39:32 +01:00
float closestPriority = 0 ;
2019-09-11 18:44:05 +02:00
foreach ( var child in children ) {
2022-04-14 18:01:30 +02:00
if ( child = = this . SelectedElement )
2019-09-11 18:44:05 +02:00
continue ;
2022-03-10 16:03:09 +01:00
var ( xOffset , yOffset ) = child . Area . Center - this . SelectedElement . Area . Center ;
2022-05-26 11:39:55 +02:00
var angle = Math . Abs ( MathHelper . WrapAngle ( direction . Angle ( ) - ( float ) Math . Atan2 ( yOffset , xOffset ) ) ) ;
2022-03-14 15:39:32 +01:00
if ( angle > = MathHelper . PiOver2 - Element . Epsilon )
2022-03-10 14:25:41 +01:00
continue ;
2022-03-10 16:03:09 +01:00
var distSq = child . Area . DistanceSquared ( this . SelectedElement . Area ) ;
2022-03-14 15:39:32 +01:00
// both distance and angle play a role in a destination button's priority, so we combine them
2022-03-14 15:59:22 +01:00
var priority = ( distSq + 1 ) * ( angle / MathHelper . PiOver2 + 1 ) ;
2022-03-14 15:39:32 +01:00
if ( closest = = null | | priority < closestPriority ) {
2019-09-11 18:44:05 +02:00
closest = child ;
2022-03-14 15:39:32 +01:00
closestPriority = priority ;
2019-09-11 18:44:05 +02:00
}
}
return closest ;
}
}
2022-04-30 12:14:08 +02:00
private bool HandleGamepadNextElement ( Direction2 dir ) {
2019-09-09 17:12:36 +02:00
this . IsAutoNavMode = true ;
2022-03-10 14:25:41 +01:00
var next = this . GetGamepadNextElement ( dir ) ;
2019-09-09 17:12:36 +02:00
if ( this . SelectedElement ! = null )
next = this . SelectedElement . GetGamepadNextElement ( dir , next ) ;
2022-04-30 12:14:08 +02:00
if ( next ! = null ) {
2020-03-17 15:04:36 +01:00
this . SelectElement ( this . ActiveRoot , next ) ;
2022-04-30 12:14:08 +02:00
return true ;
}
return false ;
2019-09-09 17:12:36 +02:00
}
2019-08-28 16:38:58 +02:00
}
2022-06-17 18:23:47 +02:00
}