From c178afdf6ba6deedccb356773fae242422609b9b Mon Sep 17 00:00:00 2001 From: Wojciech Figat Date: Tue, 21 Dec 2021 18:12:54 +0100 Subject: [PATCH] Add **UI navigation** system #574 --- Source/Engine/UI/GUI/CanvasRootControl.cs | 75 +++++++++ Source/Engine/UI/GUI/Common/Button.cs | 16 +- Source/Engine/UI/GUI/Common/CheckBox.cs | 112 ++++++++++--- Source/Engine/UI/GUI/Common/Dropdown.cs | 95 +++++++++-- Source/Engine/UI/GUI/Common/Image.cs | 2 +- Source/Engine/UI/GUI/Common/Label.cs | 2 +- .../Engine/UI/GUI/Common/RichTextBoxBase.cs | 2 +- Source/Engine/UI/GUI/Common/TextBox.cs | 2 +- Source/Engine/UI/GUI/Common/TextBoxBase.cs | 28 +++- Source/Engine/UI/GUI/ContainerControl.cs | 153 +++++++++++++++++- Source/Engine/UI/GUI/Control.cs | 127 ++++++++++++++- Source/Engine/UI/GUI/Enums.cs | 36 +++++ Source/Engine/UI/GUI/Panels/AlphaPanel.cs | 8 + Source/Engine/UI/GUI/Panels/BlurPanel.cs | 1 + Source/Engine/UI/GUI/Panels/DropPanel.cs | 9 ++ Source/Engine/UI/GUI/Panels/GridPanel.cs | 1 + .../Engine/UI/GUI/Panels/UniformGridPanel.cs | 1 + Source/Engine/UI/GUI/RootControl.cs | 56 +++++++ Source/Engine/UI/UICanvas.cs | 97 +++++++++++ Source/Engine/UI/UIControl.cpp | 29 ++++ Source/Engine/UI/UIControl.cs | 105 ++++++++++++ Source/Engine/UI/UIControl.h | 15 +- 22 files changed, 918 insertions(+), 54 deletions(-) diff --git a/Source/Engine/UI/GUI/CanvasRootControl.cs b/Source/Engine/UI/GUI/CanvasRootControl.cs index b40a9a3bf..dc32a8e62 100644 --- a/Source/Engine/UI/GUI/CanvasRootControl.cs +++ b/Source/Engine/UI/GUI/CanvasRootControl.cs @@ -1,5 +1,7 @@ // Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. +using System; + namespace FlaxEngine.GUI { /// @@ -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; /// /// Gets the owning canvas. @@ -137,6 +141,77 @@ namespace FlaxEngine.GUI && (_canvas.TestCanvasIntersection == null || _canvas.TestCanvasIntersection(ref location)); } + /// + 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; + } + } + /// public override bool OnCharInput(char c) { diff --git a/Source/Engine/UI/GUI/Common/Button.cs b/Source/Engine/UI/GUI/Common/Button.cs index d08d03dec..80e53abba 100644 --- a/Source/Engine/UI/GUI/Common/Button.cs +++ b/Source/Engine/UI/GUI/Common/Button.cs @@ -155,7 +155,7 @@ namespace FlaxEngine.GUI } /// - /// Called when mouse clicks the button. + /// Called when mouse or touch clicks the button. /// protected virtual void OnClick() { @@ -164,7 +164,7 @@ namespace FlaxEngine.GUI } /// - /// 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). /// protected virtual void OnPressBegin() { @@ -174,7 +174,7 @@ namespace FlaxEngine.GUI } /// - /// 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). /// 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(); } + + /// + public override void OnSubmit() + { + OnClick(); + + base.OnSubmit(); + } } } diff --git a/Source/Engine/UI/GUI/Common/CheckBox.cs b/Source/Engine/UI/GUI/Common/CheckBox.cs index e7ddb9a9a..609cde0ce 100644 --- a/Source/Engine/UI/GUI/Common/CheckBox.cs +++ b/Source/Engine/UI/GUI/Common/CheckBox.cs @@ -32,9 +32,9 @@ namespace FlaxEngine.GUI public class CheckBox : Control { /// - /// The mouse is down. + /// True if checked is being pressed (by mouse or touch). /// - protected bool _mouseDown; + protected bool _isPressed; /// /// The current state. @@ -186,6 +186,32 @@ namespace FlaxEngine.GUI _box = new Rectangle(0, (Height - _boxSize) * 0.5f, _boxSize, _boxSize); } + /// + /// Called when mouse or touch clicks the checkbox. + /// + protected virtual void OnClick() + { + Toggle(); + } + + /// + /// Called when checkbox starts to be pressed by the used (via mouse or touch). + /// + protected virtual void OnPressBegin() + { + _isPressed = true; + if (AutoFocus) + Focus(); + } + + /// + /// Called when checkbox ends to be pressed by the used (via mouse or touch). + /// + protected virtual void OnPressEnd() + { + _isPressed = false; + } + /// 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 /// 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 /// 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 /// public override void OnMouseLeave() { - base.OnMouseLeave(); - - // Clear flags + if (_isPressed) + OnPressEnd(); _mouseOverBox = false; - _mouseDown = false; + + base.OnMouseLeave(); + } + + /// + public override bool OnTouchDown(Vector2 location, int pointerId) + { + if (!_isPressed) + { + OnPressBegin(); + return true; + } + + return base.OnTouchDown(location, pointerId); + } + + /// + public override bool OnTouchUp(Vector2 location, int pointerId) + { + if (_isPressed) + { + OnPressEnd(); + if (_box.Contains(ref location)) + { + OnClick(); + return true; + } + } + + return base.OnTouchUp(location, pointerId); + } + + /// + public override void OnTouchLeave() + { + if (_isPressed) + OnPressEnd(); + + base.OnTouchLeave(); + } + + /// + public override void OnLostFocus() + { + if (_isPressed) + OnPressEnd(); + + base.OnLostFocus(); } /// @@ -274,5 +340,13 @@ namespace FlaxEngine.GUI CacheBox(); } + + /// + public override void OnSubmit() + { + OnClick(); + + base.OnSubmit(); + } } } diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index 04260e32f..385971fb5 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -24,11 +24,57 @@ namespace FlaxEngine.GUI public Action LostFocus; /// - 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; + } + + /// + public override Control OnNavigate(NavDirection direction, Vector2 location, Control caller, List 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); + } + + /// + public override void OnSubmit() + { + Defocus(); + + base.OnSubmit(); } /// @@ -43,14 +89,13 @@ namespace FlaxEngine.GUI [HideInEditor] private class DropdownLabel : Label { - public int Index; - public Action ItemClicked; + public Action