Add **UI navigation** system

#574
This commit is contained in:
Wojciech Figat
2021-12-21 18:12:54 +01:00
parent 71212420f6
commit c178afdf6b
22 changed files with 918 additions and 54 deletions

View File

@@ -1,5 +1,7 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
namespace FlaxEngine.GUI
{
/// <summary>
@@ -11,6 +13,8 @@ namespace FlaxEngine.GUI
{
private UICanvas _canvas;
private Vector2 _mousePosition;
private float _navigationHeldTimeUp, _navigationHeldTimeDown, _navigationHeldTimeLeft, _navigationHeldTimeRight, _navigationHeldTimeSubmit;
private float _navigationRateTimeUp, _navigationRateTimeDown, _navigationRateTimeLeft, _navigationRateTimeRight, _navigationRateTimeSubmit;
/// <summary>
/// Gets the owning canvas.
@@ -137,6 +141,77 @@ namespace FlaxEngine.GUI
&& (_canvas.TestCanvasIntersection == null || _canvas.TestCanvasIntersection(ref location));
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
// UI navigation
if (_canvas.ReceivesEvents)
{
UpdateNavigation(deltaTime, _canvas.NavigationInputActionUp, NavDirection.Up, ref _navigationHeldTimeUp, ref _navigationRateTimeUp);
UpdateNavigation(deltaTime, _canvas.NavigationInputActionDown, NavDirection.Down, ref _navigationHeldTimeDown, ref _navigationRateTimeDown);
UpdateNavigation(deltaTime, _canvas.NavigationInputActionLeft, NavDirection.Left, ref _navigationHeldTimeLeft, ref _navigationRateTimeLeft);
UpdateNavigation(deltaTime, _canvas.NavigationInputActionRight, NavDirection.Right, ref _navigationHeldTimeRight, ref _navigationRateTimeRight);
UpdateNavigation(deltaTime, _canvas.NavigationInputActionSubmit, ref _navigationHeldTimeSubmit, ref _navigationRateTimeSubmit, SubmitFocused);
}
else
{
_navigationHeldTimeUp = _navigationHeldTimeDown = _navigationHeldTimeLeft = _navigationHeldTimeRight = 0;
_navigationRateTimeUp = _navigationRateTimeDown = _navigationRateTimeLeft = _navigationRateTimeRight = 0;
}
base.Update(deltaTime);
}
private void UpdateNavigation(float deltaTime, string actionName, NavDirection direction, ref float heldTime, ref float rateTime)
{
if (Input.GetAction(actionName))
{
if (heldTime <= Mathf.Epsilon)
{
Navigate(direction);
}
if (heldTime > _canvas.NavigationInputRepeatDelay)
{
rateTime += deltaTime;
}
if (rateTime > _canvas.NavigationInputRepeatRate)
{
Navigate(direction);
rateTime = 0;
}
heldTime += deltaTime;
}
else
{
heldTime = rateTime = 0;
}
}
private void UpdateNavigation(float deltaTime, string actionName, ref float heldTime, ref float rateTime, Action action)
{
if (Input.GetAction(actionName))
{
if (heldTime <= Mathf.Epsilon)
{
action();
}
if (heldTime > _canvas.NavigationInputRepeatDelay)
{
rateTime += deltaTime;
}
if (rateTime > _canvas.NavigationInputRepeatRate)
{
action();
rateTime = 0;
}
heldTime += deltaTime;
}
else
{
heldTime = rateTime = 0;
}
}
/// <inheritdoc />
public override bool OnCharInput(char c)
{

View File

@@ -155,7 +155,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Called when mouse clicks the button.
/// Called when mouse or touch clicks the button.
/// </summary>
protected virtual void OnClick()
{
@@ -164,7 +164,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Called when buttons starts to be pressed by the used (via mouse or touch).
/// Called when button starts to be pressed by the used (via mouse or touch).
/// </summary>
protected virtual void OnPressBegin()
{
@@ -174,7 +174,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Called when buttons ends to be pressed by the used (via mouse or touch).
/// Called when button ends to be pressed by the used (via mouse or touch).
/// </summary>
protected virtual void OnPressEnd()
{
@@ -215,7 +215,7 @@ namespace FlaxEngine.GUI
backgroundColor = BackgroundColorSelected;
borderColor = BorderColorSelected;
}
else if (IsMouseOver)
else if (IsMouseOver || IsNavFocused)
{
backgroundColor = BackgroundColorHighlighted;
borderColor = BorderColorHighlighted;
@@ -322,5 +322,13 @@ namespace FlaxEngine.GUI
base.OnLostFocus();
}
/// <inheritdoc />
public override void OnSubmit()
{
OnClick();
base.OnSubmit();
}
}
}

View File

@@ -32,9 +32,9 @@ namespace FlaxEngine.GUI
public class CheckBox : Control
{
/// <summary>
/// The mouse is down.
/// True if checked is being pressed (by mouse or touch).
/// </summary>
protected bool _mouseDown;
protected bool _isPressed;
/// <summary>
/// The current state.
@@ -186,6 +186,32 @@ namespace FlaxEngine.GUI
_box = new Rectangle(0, (Height - _boxSize) * 0.5f, _boxSize, _boxSize);
}
/// <summary>
/// Called when mouse or touch clicks the checkbox.
/// </summary>
protected virtual void OnClick()
{
Toggle();
}
/// <summary>
/// Called when checkbox starts to be pressed by the used (via mouse or touch).
/// </summary>
protected virtual void OnPressBegin()
{
_isPressed = true;
if (AutoFocus)
Focus();
}
/// <summary>
/// Called when checkbox ends to be pressed by the used (via mouse or touch).
/// </summary>
protected virtual void OnPressEnd()
{
_isPressed = false;
}
/// <inheritdoc />
public override void Draw()
{
@@ -197,7 +223,7 @@ namespace FlaxEngine.GUI
Color borderColor = BorderColor;
if (!enabled)
borderColor *= 0.5f;
else if (_mouseDown || _mouseOverBox)
else if (_isPressed || _mouseOverBox || IsNavFocused)
borderColor = BorderColorHighlighted;
Render2D.DrawRectangle(_box.MakeExpanded(-2.0f), borderColor);
@@ -226,11 +252,9 @@ namespace FlaxEngine.GUI
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
if (button == MouseButton.Left && !_isPressed)
{
// Set flag
_mouseDown = true;
Focus();
OnPressBegin();
return true;
}
@@ -240,16 +264,12 @@ namespace FlaxEngine.GUI
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left && _mouseDown)
if (button == MouseButton.Left && _isPressed)
{
// Clear flag
_mouseDown = false;
// Check if mouse is still over the box
if (_mouseOverBox)
OnPressEnd();
if (_box.Contains(ref location))
{
Toggle();
Focus();
OnClick();
return true;
}
}
@@ -260,11 +280,57 @@ namespace FlaxEngine.GUI
/// <inheritdoc />
public override void OnMouseLeave()
{
base.OnMouseLeave();
// Clear flags
if (_isPressed)
OnPressEnd();
_mouseOverBox = false;
_mouseDown = false;
base.OnMouseLeave();
}
/// <inheritdoc />
public override bool OnTouchDown(Vector2 location, int pointerId)
{
if (!_isPressed)
{
OnPressBegin();
return true;
}
return base.OnTouchDown(location, pointerId);
}
/// <inheritdoc />
public override bool OnTouchUp(Vector2 location, int pointerId)
{
if (_isPressed)
{
OnPressEnd();
if (_box.Contains(ref location))
{
OnClick();
return true;
}
}
return base.OnTouchUp(location, pointerId);
}
/// <inheritdoc />
public override void OnTouchLeave()
{
if (_isPressed)
OnPressEnd();
base.OnTouchLeave();
}
/// <inheritdoc />
public override void OnLostFocus()
{
if (_isPressed)
OnPressEnd();
base.OnLostFocus();
}
/// <inheritdoc />
@@ -274,5 +340,13 @@ namespace FlaxEngine.GUI
CacheBox();
}
/// <inheritdoc />
public override void OnSubmit()
{
OnClick();
base.OnSubmit();
}
}
}

View File

@@ -24,11 +24,57 @@ namespace FlaxEngine.GUI
public Action LostFocus;
/// <inheritdoc />
public override void OnLostFocus()
public override void OnEndContainsFocus()
{
base.OnLostFocus();
base.OnEndContainsFocus();
LostFocus?.Invoke();
// Call event after this 'focus contains flag' propagation ends to prevent focus issues
if (LostFocus != null)
Scripting.RunOnUpdate(LostFocus);
}
private static DropdownLabel FindItem(Control control)
{
if (control is DropdownLabel item)
return item;
if (control is ContainerControl containerControl)
{
foreach (var child in containerControl.Children)
{
item = FindItem(child);
if (item != null)
return item;
}
}
return null;
}
/// <inheritdoc />
public override Control OnNavigate(NavDirection direction, Vector2 location, Control caller, List<Control> visited)
{
if (IsFocused)
{
// Dropdown root is focused
if (direction == NavDirection.Down)
{
// Pick the first item
return FindItem(this);
}
// Close popup
Defocus();
return null;
}
return base.OnNavigate(direction, location, caller, visited);
}
/// <inheritdoc />
public override void OnSubmit()
{
Defocus();
base.OnSubmit();
}
/// <inheritdoc />
@@ -43,14 +89,13 @@ namespace FlaxEngine.GUI
[HideInEditor]
private class DropdownLabel : Label
{
public int Index;
public Action<int> ItemClicked;
public Action<Label> ItemClicked;
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
ItemClicked?.Invoke(Index);
ItemClicked?.Invoke(this);
return true;
}
@@ -58,10 +103,18 @@ namespace FlaxEngine.GUI
{
if (base.OnTouchDown(location, pointerId))
return true;
ItemClicked?.Invoke(Index);
ItemClicked?.Invoke(this);
return true;
}
/// <inheritdoc />
public override void OnSubmit()
{
ItemClicked?.Invoke(this);
base.OnSubmit();
}
public override void OnDestroy()
{
ItemClicked = null;
@@ -81,6 +134,7 @@ namespace FlaxEngine.GUI
protected DropdownRoot _popup;
private bool _touchDown;
private bool _hadNavFocus;
/// <summary>
/// The selected index of the item (-1 for no selection).
@@ -229,8 +283,6 @@ namespace FlaxEngine.GUI
public Dropdown()
: base(0, 0, 120, 18.0f)
{
AutoFocus = false;
var style = Style.Current;
Font = new FontReference(style.FontMedium);
TextColor = style.Foreground;
@@ -346,8 +398,9 @@ namespace FlaxEngine.GUI
for (int i = 0; i < _items.Count; i++)
{
var item = new Spacer
var item = new ContainerControl
{
AutoFocus = false,
Height = itemsHeight,
Width = itemsWidth,
Parent = container,
@@ -355,6 +408,7 @@ namespace FlaxEngine.GUI
var label = new DropdownLabel
{
AutoFocus = true,
X = itemsMargin,
Size = new Vector2(itemsWidth - itemsMargin, itemsHeight),
Font = Font,
@@ -363,11 +417,11 @@ namespace FlaxEngine.GUI
HorizontalAlignment = TextAlignment.Near,
Text = _items[i],
Parent = item,
Index = i,
Tag = i,
};
label.ItemClicked += index =>
label.ItemClicked += c =>
{
OnItemClicked(index);
OnItemClicked((int)c.Tag);
DestroyPopup();
};
height += itemsHeight;
@@ -416,6 +470,10 @@ namespace FlaxEngine.GUI
OnPopupHide();
_popup.Dispose();
_popup = null;
if (_hadNavFocus)
NavigationFocus();
else
Focus();
}
}
@@ -430,6 +488,7 @@ namespace FlaxEngine.GUI
// Setup popup
DestroyPopup();
_hadNavFocus = IsNavFocused;
_popup = CreatePopup();
_popup.UnlockChildrenRecursive();
_popup.PerformLayout();
@@ -488,7 +547,7 @@ namespace FlaxEngine.GUI
borderColor = BorderColorSelected;
arrowColor = ArrowColorSelected;
}
else if (IsMouseOver)
else if (IsMouseOver || IsNavFocused)
{
backgroundColor = BackgroundColorHighlighted;
borderColor = BorderColorHighlighted;
@@ -590,5 +649,13 @@ namespace FlaxEngine.GUI
base.OnTouchLeave(pointerId);
}
/// <inheritdoc />
public override void OnSubmit()
{
ShowPopup();
base.OnSubmit();
}
}
}

View File

@@ -114,7 +114,7 @@ namespace FlaxEngine.GUI
Margin.ShrinkRectangle(ref rect);
var color = IsMouseOver ? MouseOverColor : Color;
var color = IsMouseOver || IsNavFocused ? MouseOverColor : Color;
if (!Enabled)
color *= DisabledTint;
Brush.Draw(rect, color);

View File

@@ -210,7 +210,7 @@ namespace FlaxEngine.GUI
if (ClipText)
Render2D.PushClip(new Rectangle(Vector2.Zero, Size));
var color = IsMouseOver ? TextColorHighlighted : TextColor;
var color = IsMouseOver || IsNavFocused ? TextColorHighlighted : TextColor;
if (!EnabledInHierarchy)
color *= 0.6f;

View File

@@ -228,7 +228,7 @@ namespace FlaxEngine.GUI
// Background
Color backColor = BackgroundColor;
if (IsMouseOver)
if (IsMouseOver || IsNavFocused)
backColor = BackgroundSelectedColor;
Render2D.FillRectangle(rect, backColor);
Render2D.DrawRectangle(rect, IsFocused ? BorderSelectedColor : BorderColor);

View File

@@ -152,7 +152,7 @@ namespace FlaxEngine.GUI
// Background
Color backColor = BackgroundColor;
if (IsMouseOver)
if (IsMouseOver || IsNavFocused)
backColor = BackgroundSelectedColor;
Render2D.FillRectangle(rect, backColor);
Render2D.DrawRectangle(rect, IsFocused ? BorderSelectedColor : BorderColor);

View File

@@ -431,7 +431,6 @@ namespace FlaxEngine.GUI
_isMultiline = isMultiline;
_maxLength = 2147483646;
_selectionStart = _selectionEnd = -1;
AutoFocus = false;
var style = Style.Current;
CaretColor = style.Foreground;
@@ -1015,6 +1014,28 @@ namespace FlaxEngine.GUI
_isSelecting = false;
}
/// <inheritdoc />
public override void NavigationFocus()
{
base.NavigationFocus();
if (IsNavFocused)
SelectAll();
}
/// <inheritdoc />
public override void OnSubmit()
{
OnEditEnd();
if (IsNavFocused)
{
OnEditBegin();
SelectAll();
}
base.OnSubmit();
}
/// <inheritdoc />
public override void OnMouseMove(Vector2 location)
{
@@ -1215,7 +1236,8 @@ namespace FlaxEngine.GUI
SetSelection(-1);
_text = _onStartEditValue;
Defocus();
if (!IsNavFocused)
Defocus();
OnTextChanged();
return true;
@@ -1226,7 +1248,7 @@ namespace FlaxEngine.GUI
// Insert new line
Insert('\n');
}
else
else if (!IsNavFocused)
{
// End editing
Defocus();

View File

@@ -491,6 +491,153 @@ namespace FlaxEngine.GUI
return child.IntersectsContent(ref location, out childSpaceLocation);
}
#region Navigation
/// <inheritdoc />
public override Control OnNavigate(NavDirection direction, Vector2 location, Control caller, List<Control> visited)
{
// Try to focus itself first (only if navigation focus can enter this container)
if (AutoFocus && !ContainsFocus)
return this;
// Try to focus children
if (_children.Count != 0 && !visited.Contains(this))
{
visited.Add(this);
// Perform automatic navigation based on the layout
var result = NavigationRaycast(direction, location, visited);
if (result == null && direction == NavDirection.Next)
{
// Try wrap the navigation over the layout based on the direction
var visitedWrap = new List<Control>(visited);
result = NavigationWrap(direction, location, visitedWrap);
}
if (result != null)
{
result = result.OnNavigate(direction, result.PointFromParent(location), this, visited);
if (result != null)
return result;
}
}
// Try to focus itself
if (AutoFocus && !IsFocused || caller == this)
return this;
// Route navigation to parent
var parent = Parent;
if (AutoFocus && Visible)
{
// Focusable container controls use own nav origin instead of the provided one
location = GetNavOrigin(direction);
}
return parent?.OnNavigate(direction, PointToParent(location), caller, visited);
}
/// <summary>
/// Checks if this container control can more with focus navigation into the given child control.
/// </summary>
/// <param name="child">The child.</param>
/// <returns>True if can navigate to it, otherwise false.</returns>
protected virtual bool CanNavigateChild(Control child)
{
return !child.IsFocused && child.Enabled && child.Visible && CanGetAutoFocus(child);
}
/// <summary>
/// Wraps the navigation over the layout.
/// </summary>
/// <param name="direction">The navigation direction.</param>
/// <param name="location">The navigation start location (in the control-space).</param>
/// <param name="visited">The list with visited controls. Used to skip recursive navigation calls when doing traversal across the UI hierarchy.</param>
/// <returns>The target navigation control or null if didn't performed any navigation.</returns>
protected virtual Control NavigationWrap(NavDirection direction, Vector2 location, List<Control> visited)
{
// This searches form a child that calls this navigation event (see Control.OnNavigate) to determinate the layout wrapping size based on that child size
var currentChild = RootWindow?.FocusedControl;
visited.Add(this);
if (currentChild != null)
{
var layoutSize = currentChild.Size;
var predictedLocation = Vector2.Minimum;
switch (direction)
{
case NavDirection.Next:
predictedLocation = new Vector2(0, location.Y + layoutSize.Y);
break;
}
if (new Rectangle(Vector2.Zero, Size).Contains(ref predictedLocation))
{
var result = NavigationRaycast(direction, predictedLocation, visited);
if (result != null)
return result;
}
}
return Parent?.NavigationWrap(direction, PointToParent(ref location), visited);
}
private static bool CanGetAutoFocus(Control c)
{
if (c.AutoFocus)
return true;
if (c is ContainerControl cc)
{
for (int i = 0; i < cc.Children.Count; i++)
{
if (cc.CanNavigateChild(cc.Children[i]))
return true;
}
}
return false;
}
private Control NavigationRaycast(NavDirection direction, Vector2 location, List<Control> visited)
{
Vector2 uiDir1 = Vector2.Zero, uiDir2 = Vector2.Zero;
switch (direction)
{
case NavDirection.Up:
uiDir1 = uiDir2 = new Vector2(0, -1);
break;
case NavDirection.Down:
uiDir1 = uiDir2 = new Vector2(0, 1);
break;
case NavDirection.Left:
uiDir1 = uiDir2 = new Vector2(-1, 0);
break;
case NavDirection.Right:
uiDir1 = uiDir2 = new Vector2(1, 0);
break;
case NavDirection.Next:
uiDir1 = new Vector2(1, 0);
uiDir2 = new Vector2(0, 1);
break;
}
Control result = null;
var minDistance = float.MaxValue;
for (var i = 0; i < _children.Count; i++)
{
var child = _children[i];
if (!CanNavigateChild(child) || visited.Contains(child))
continue;
var childNavLocation = child.Center;
var childBounds = child.Bounds;
var childNavDirection = Vector2.Normalize(childNavLocation - location);
var childNavCoherence1 = Vector2.Dot(ref uiDir1, ref childNavDirection);
var childNavCoherence2 = Vector2.Dot(ref uiDir2, ref childNavDirection);
var distance = Rectangle.Distance(childBounds, location);
if (childNavCoherence1 > Mathf.Epsilon && childNavCoherence2 > Mathf.Epsilon && distance < minDistance)
{
minDistance = distance;
result = child;
}
}
return result;
}
#endregion
/// <summary>
/// Update contain focus state and all it's children
/// </summary>
@@ -501,10 +648,10 @@ namespace FlaxEngine.GUI
for (int i = 0; i < _children.Count; i++)
{
if (_children[i] is ContainerControl child)
var control = _children[i];
if (control is ContainerControl child)
child.UpdateContainsFocus();
if (_children[i].ContainsFocus)
if (control.ContainsFocus)
result = true;
}

View File

@@ -51,7 +51,7 @@ namespace FlaxEngine.GUI
private ContainerControl _parent;
private RootControl _root;
private bool _isDisposing, _isFocused;
private bool _isDisposing, _isFocused, _isNavFocused;
// State
@@ -456,9 +456,9 @@ namespace FlaxEngine.GUI
#region Focus
/// <summary>
/// Gets a value indicating whether the control can receive automatic focus on user events (eg. mouse down).
/// If checked, the control can receive automatic focus (eg. on user click or UI navigation).
/// </summary>
[HideInEditor, NoSerialize]
[EditorOrder(512), Tooltip("If checked, the control can receive automatic focus (eg. on user click or UI navigation).")]
public bool AutoFocus
{
get => _autoFocus;
@@ -473,7 +473,12 @@ namespace FlaxEngine.GUI
/// <summary>
/// Gets a value indicating whether the control has input focus
/// </summary>
public virtual bool IsFocused => _isFocused;
public bool IsFocused => _isFocused;
/// <summary>
/// Gets a value indicating whether the control has UI navigation focus.
/// </summary>
public bool IsNavFocused => _isNavFocused;
/// <summary>
/// Sets input focus to the control
@@ -505,6 +510,7 @@ namespace FlaxEngine.GUI
{
// Cache flag
_isFocused = true;
_isNavFocused = false;
}
/// <summary>
@@ -515,6 +521,7 @@ namespace FlaxEngine.GUI
{
// Clear flag
_isFocused = false;
_isNavFocused = false;
}
/// <summary>
@@ -575,6 +582,118 @@ namespace FlaxEngine.GUI
#endregion
#region Navigation
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Up"/> direction.
/// </summary>
[HideInEditor, NoSerialize]
public Control NavTargetUp;
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Down"/> direction.
/// </summary>
[HideInEditor, NoSerialize]
public Control NavTargetDown;
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Left"/> direction.
/// </summary>
[HideInEditor, NoSerialize]
public Control NavTargetLeft;
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Right"/> direction.
/// </summary>
[HideInEditor, NoSerialize]
public Control NavTargetRight;
/// <summary>
/// Gets the next navigation control to focus for the given direction. Returns null for automated direction resolving.
/// </summary>
/// <param name="direction">The navigation direction.</param>
/// <returns>The target navigation control or null to use automatic navigation.</returns>
[NoAnimate]
public virtual Control GetNavTarget(NavDirection direction)
{
switch (direction)
{
case NavDirection.Up: return NavTargetUp;
case NavDirection.Down: return NavTargetDown;
case NavDirection.Left: return NavTargetLeft;
case NavDirection.Right: return NavTargetRight;
default: return null;
}
}
/// <summary>
/// Gets the navigation origin location for this control. It's the starting anchor point for searching navigable controls in the nearby area. By default the origin points are located on the control bounds edges.
/// </summary>
/// <param name="direction">The navigation direction.</param>
/// <returns>The navigation origin for the automatic navigation.</returns>
[NoAnimate]
public virtual Vector2 GetNavOrigin(NavDirection direction)
{
var size = Size;
switch (direction)
{
case NavDirection.Up: return new Vector2(size.X * 0.5f, 0);
case NavDirection.Down: return new Vector2(size.X * 0.5f, size.Y);
case NavDirection.Left: return new Vector2(0, size.Y * 0.5f);
case NavDirection.Right: return new Vector2(size.X, size.Y * 0.5f);
case NavDirection.Next: return Vector2.Zero;
default: return size * 0.5f;
}
}
/// <summary>
/// Performs the UI navigation for this control.
/// </summary>
/// <param name="direction">The navigation direction.</param>
/// <param name="location">The navigation start location (in the control-space).</param>
/// <param name="caller">The control that calls the event.</param>
/// <param name="visited">The list with visited controls. Used to skip recursive navigation calls when doing traversal across the UI hierarchy.</param>
/// <returns>The target navigation control or null if didn't performed any navigation.</returns>
public virtual Control OnNavigate(NavDirection direction, Vector2 location, Control caller, List<Control> visited)
{
if (caller == _parent && AutoFocus && Visible)
return this;
return _parent?.OnNavigate(direction, PointToParent(GetNavOrigin(direction)), caller, visited);
}
/// <summary>
/// Focuses the control by the UI navigation system. Called during navigating around UI with gamepad/keyboard navigation. Focuses the control and sets the <see cref="IsNavFocused"/> flag.
/// </summary>
public virtual void NavigationFocus()
{
Focus();
if (IsFocused)
{
_isNavFocused = true;
// Ensure to be in a view
var parent = Parent;
while (parent != null)
{
if (parent is Panel panel && ((panel.VScrollBar != null && panel.VScrollBar.Enabled) || (panel.HScrollBar != null && panel.HScrollBar.Enabled)))
{
panel.ScrollViewTo(this);
break;
}
parent = parent.Parent;
}
}
}
/// <summary>
/// Generic user interaction event for a control used by UI navigation (eg. user submits on the currently focused control).
/// </summary>
public virtual void OnSubmit()
{
}
#endregion
#region Mouse
/// <summary>

View File

@@ -167,4 +167,40 @@ namespace FlaxEngine.GUI
/// </summary>
Vertical = 1,
}
/// <summary>
/// The navigation directions in the user interface layout.
/// </summary>
public enum NavDirection
{
/// <summary>
/// No direction to skip navigation.
/// </summary>
None,
/// <summary>
/// The up direction.
/// </summary>
Up,
/// <summary>
/// The down direction.
/// </summary>
Down,
/// <summary>
/// The left direction.
/// </summary>
Left,
/// <summary>
/// The right direction.
/// </summary>
Right,
/// <summary>
/// The next item (right with layout wrapping).
/// </summary>
Next,
}
}

View File

@@ -19,6 +19,14 @@ namespace FlaxEngine.GUI
[EditorOrder(10), Tooltip("Whether or not we should ignore previous alphas.")]
public bool IgnoreStack;
/// <summary>
/// Initializes a new instance of the <see cref="AlphaPanel"/> class.
/// </summary>
public AlphaPanel()
{
AutoFocus = false;
}
/// <inheritdoc/>
public override void Draw()
{

View File

@@ -19,6 +19,7 @@ namespace FlaxEngine.GUI
/// </summary>
public BlurPanel()
{
AutoFocus = false;
}
/// <inheritdoc />

View File

@@ -560,5 +560,14 @@ namespace FlaxEngine.GUI
{
Arrange();
}
/// <inheritdoc />
protected override bool CanNavigateChild(Control child)
{
// Closed panel skips navigation for hidden children
if (IsClosed && child.IsScrollable)
return false;
return base.CanNavigateChild(child);
}
}
}

View File

@@ -73,6 +73,7 @@ namespace FlaxEngine.GUI
/// <param name="slotPadding">The slot padding.</param>
public GridPanel(float slotPadding)
{
AutoFocus = false;
SlotPadding = new Margin(slotPadding);
_cellsH = new[]
{

View File

@@ -75,6 +75,7 @@ namespace FlaxEngine.GUI
/// <param name="slotPadding">The slot padding.</param>
public UniformGridPanel(float slotPadding = 2)
{
AutoFocus = false;
SlotPadding = new Margin(slotPadding);
_slotsH = _slotsV = 5;
}

View File

@@ -1,5 +1,6 @@
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
namespace FlaxEngine.GUI
@@ -69,6 +70,61 @@ namespace FlaxEngine.GUI
AutoFocus = false;
}
#region Navigation
/// <summary>
/// The custom callback function for UI navigation. Can be used to override the default behaviour.
/// </summary>
public Action<NavDirection> CustomNavigation;
/// <summary>
/// Performs the UI navigation.
/// </summary>
/// <param name="direction">The navigation direction.</param>
public void Navigate(NavDirection direction)
{
if (direction == NavDirection.None)
return;
if (CustomNavigation != null)
{
// Custom
CustomNavigation.Invoke(direction);
return;
}
var focused = FocusedControl;
if (focused == null)
{
// Nothing is focused so go to the first control
focused = OnNavigate(direction, Vector2.Zero, this, new List<Control>());
focused?.NavigationFocus();
return;
}
var target = focused.GetNavTarget(direction);
if (target != null)
{
// Explicitly specified focus target
target.NavigationFocus();
return;
}
// Automatic navigation routine
target = focused.OnNavigate(direction, focused.GetNavOrigin(direction), this, new List<Control>());
target?.NavigationFocus();
}
/// <summary>
/// Submits the currently focused control.
/// </summary>
public void SubmitFocused()
{
FocusedControl?.OnSubmit();
}
#endregion
/// <inheritdoc />
public override void Update(float deltaTime)
{