using System.Diagnostics.CodeAnalysis; using System.Numerics; using NUnit.Framework; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML.Proxy; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; namespace Robust.UnitTesting.Client.UserInterface { [TestFixture] public sealed class UserInterfaceManagerTest : RobustUnitTest { public override UnitTestProject Project => UnitTestProject.Client; private IUserInterfaceManagerInternal _userInterfaceManager = default!; [OneTimeSetUp] public void Setup() { _userInterfaceManager = IoCManager.Resolve(); _userInterfaceManager.InitializeTesting(); IoCManager.Resolve().Initialize(); } [Test] [SuppressMessage("ReSharper", "AccessToModifiedClosure")] public void TestMouseDown() { // We create 4 controls. // Control 2 is set to stop mouse events, // Control 3 pass, // Control 4 ignore. // We check that 4 and 1 do not receive events, that 3 receives before 2, and that positions are correct. var control1 = new LayoutContainer { MinSize = new Vector2(50, 50) }; var control2 = new LayoutContainer { MinSize = new Vector2(50, 50), MouseFilter = Control.MouseFilterMode.Stop }; var control3 = new LayoutContainer { MinSize = new Vector2(50, 50), MouseFilter = Control.MouseFilterMode.Pass }; var control4 = new LayoutContainer { MinSize = new Vector2(50, 50), MouseFilter = Control.MouseFilterMode.Ignore }; _userInterfaceManager.RootControl.AddChild(control1); control1.AddChild(control2); // Offsets to test relative positioning on the events. LayoutContainer.SetPosition(control2, new Vector2(5, 5)); control2.AddChild(control3); LayoutContainer.SetPosition(control3, new Vector2(5, 5)); control3.AddChild(control4); LayoutContainer.SetPosition(control4, new Vector2(5, 5)); control1.Arrange(new UIBox2(0, 0, 50, 50)); var mouseEvent = new BoundKeyEventArgs(EngineKeyFunctions.Use, BoundKeyState.Down, new ScreenCoordinates(30, 30, WindowId.Main), true); var control2Fired = false; var control3Fired = false; control1.OnKeyBindDown += _ => Assert.Fail("Control 1 should not get a mouse event."); void Control2MouseDown(GUIBoundKeyEventArgs ev) { Assert.That(control2Fired, NUnit.Framework.Is.False); Assert.That(control3Fired, NUnit.Framework.Is.True); Assert.That(ev.RelativePosition, NUnit.Framework.Is.EqualTo(new Vector2(25, 25))); control2Fired = true; } control2.OnKeyBindDown += Control2MouseDown; control3.OnKeyBindDown += ev => { Assert.That(control2Fired, NUnit.Framework.Is.False); Assert.That(control3Fired, NUnit.Framework.Is.False); Assert.That(ev.RelativePosition, NUnit.Framework.Is.EqualTo(new Vector2(20, 20))); control3Fired = true; }; control4.OnKeyBindDown += _ => Assert.Fail("Control 4 should not get a mouse event."); _userInterfaceManager.KeyBindDown(mouseEvent); _userInterfaceManager.KeyBindUp(mouseEvent); Assert.Multiple(() => { Assert.That(control2Fired, NUnit.Framework.Is.True); Assert.That(control3Fired, NUnit.Framework.Is.True); }); // Step two: instead of relying on stop for control2 to prevent the event reaching control1, // handle the event in control2. control2Fired = false; control3Fired = false; control2.OnKeyBindDown -= Control2MouseDown; control2.OnKeyBindDown += ev => { Assert.That(control2Fired, NUnit.Framework.Is.False); Assert.That(control3Fired, NUnit.Framework.Is.True); Assert.That(ev.RelativePosition, NUnit.Framework.Is.EqualTo(new Vector2(25, 25))); control2Fired = true; ev.Handle(); }; control2.MouseFilter = Control.MouseFilterMode.Pass; _userInterfaceManager.KeyBindDown(mouseEvent); _userInterfaceManager.KeyBindUp(mouseEvent); Assert.Multiple(() => { Assert.That(control2Fired, NUnit.Framework.Is.True); Assert.That(control3Fired, NUnit.Framework.Is.True); }); control1.Orphan(); } [Test] public void TestGrabKeyboardFocus() { Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); var control1 = new Control {CanKeyboardFocus = true}; control1.GrabKeyboardFocus(); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.EqualTo(control1)); Assert.That(control1.HasKeyboardFocus(), NUnit.Framework.Is.EqualTo(true)); control1.ReleaseKeyboardFocus(); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); } [Test] public void TestGrabKeyboardFocusSteal() { Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); var control1 = new Control {CanKeyboardFocus = true}; var control2 = new Control {CanKeyboardFocus = true}; control1.GrabKeyboardFocus(); control2.GrabKeyboardFocus(); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.EqualTo(control2)); control2.ReleaseKeyboardFocus(); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); } [Test] public void TestGrabKeyboardFocusOtherRelease() { Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); var control1 = new Control {CanKeyboardFocus = true}; var control2 = new Control {CanKeyboardFocus = true}; control1.GrabKeyboardFocus(); control2.ReleaseKeyboardFocus(); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.EqualTo(control1)); _userInterfaceManager.ReleaseKeyboardFocus(); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); } [Test] public void TestGrabKeyboardFocusNull() { Assert.That(() => _userInterfaceManager.GrabKeyboardFocus(null!), Throws.ArgumentNullException); Assert.That(() => _userInterfaceManager.ReleaseKeyboardFocus(null!), Throws.ArgumentNullException); } [Test] public void TestGrabKeyboardFocusBlocked() { var control = new Control(); Assert.That(() => _userInterfaceManager.GrabKeyboardFocus(control), Throws.ArgumentException); } [Test] public void TestGrabKeyboardFocusOnClick() { var control = new Control { CanKeyboardFocus = true, KeyboardFocusOnClick = true, MinSize = new Vector2(50, 50), MouseFilter = Control.MouseFilterMode.Stop }; _userInterfaceManager.RootControl.AddChild(control); _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 50, 50)); _userInterfaceManager.HandleCanFocusDown(new ScreenCoordinates(30, 30, WindowId.Main), out _); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.EqualTo(control)); _userInterfaceManager.ReleaseKeyboardFocus(); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); control.Orphan(); } /// /// Assert that indeed nothing happens when the control has focus modes off. /// [Test] public void TestNotGrabKeyboardFocusOnClick() { var control = new Control { MinSize = new Vector2(50, 50), MouseFilter = Control.MouseFilterMode.Stop }; _userInterfaceManager.RootControl.AddChild(control); var pos = new ScreenCoordinates(30, 30, WindowId.Main); var mouseEvent = new GUIBoundKeyEventArgs(EngineKeyFunctions.Use, BoundKeyState.Down, pos, true, pos.Position / 1 - control.GlobalPosition, pos.Position - control.GlobalPixelPosition); _userInterfaceManager.KeyBindDown(mouseEvent); _userInterfaceManager.KeyBindUp(mouseEvent); Assert.That(_userInterfaceManager.KeyboardFocused, NUnit.Framework.Is.Null); control.Orphan(); } /// /// Assert that windows correctly re-position themselves if their parent is re-sized. /// [Test] public void TestWindowTracksRelativePositionOnResize() { _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 100, 100)); var window = new TestWindow { SetSize = new Vector2(20, 20), }; _userInterfaceManager.WindowRoot.AddChild(window); var initialPos = new Vector2(40, 40); LayoutContainer.SetPosition(window, initialPos); Assert.That(window.Position, Is.EqualTo(initialPos)); _userInterfaceManager.WindowRoot.InvalidateArrange(); _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 200, 200)); // 40,40 x 2 + 10 Assert.That(window.Position, Is.EqualTo(new Vector2(90, 90))); window.Orphan(); } /// /// Assert that window open/close state and events behave consistently. /// [Test] public void TestWindowOpenCloseLifecycle() { var window = new DefaultWindow(); var closeCount = 0; window.OnClose += () => closeCount++; Assert.That(window.IsOpen, Is.False); Assert.That(window.Parent, Is.Null); window.Close(); Assert.That(closeCount, Is.EqualTo(0)); window.Open(); Assert.Multiple(() => { Assert.That(window.IsOpen, Is.True); Assert.That(window.Parent, Is.SameAs(_userInterfaceManager.WindowRoot)); Assert.That(_userInterfaceManager.WindowRoot.Children, Does.Contain(window)); }); window.Close(); Assert.Multiple(() => { Assert.That(window.IsOpen, Is.False); Assert.That(window.Parent, Is.Null); Assert.That(closeCount, Is.EqualTo(1)); }); window.Close(); Assert.That(closeCount, Is.EqualTo(1)); } /// /// Assert that a DefaultWindow close button still works after the window exits the UI tree and is reopened. /// [Test] public void TestWindowCloseButtonWorksAfterReopen() { _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 100, 100)); var window = new DefaultWindow(); var closeButton = UiTestHelpers.FindDescendant( window, control => control.HasStyleClass(DefaultWindow.StyleClassWindowCloseButton)); closeButton.MinSize = new Vector2(10, 10); var closeCount = 0; window.OnClose += () => closeCount++; window.Open(); Assert.That(window.IsOpen, Is.True); UiTestHelpers.ClickButton(closeButton); Assert.Multiple(() => { Assert.That(window.IsOpen, Is.False); Assert.That(closeCount, Is.EqualTo(1)); }); window.Open(); Assert.That(window.IsOpen, Is.True); UiTestHelpers.ClickButton(closeButton); Assert.Multiple(() => { Assert.That(window.IsOpen, Is.False); Assert.That(closeCount, Is.EqualTo(2)); }); } /// /// Assert that windows can be moved by dragging them. /// [Test] public void TestWindowCanBeMovedByDragging() { _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 100, 100)); var window = new TestWindow { SetSize = new Vector2(40, 40), MouseFilter = Control.MouseFilterMode.Stop, }; _userInterfaceManager.WindowRoot.AddChild(window); window.Arrange(UIBox2.FromDimensions(new Vector2(20, 20), new Vector2(40, 40))); window.TestKeyBindDown(CreateGuiEvent(BoundKeyState.Down, new Vector2(30, 30), new Vector2(10, 10))); window.TestMouseMove(CreateMouseMoveEvent(window, new Vector2(60, 60))); window.TestKeyBindUp(CreateGuiEvent(BoundKeyState.Up, new Vector2(60, 60), new Vector2(40, 40))); Assert.That(window.Position, Is.EqualTo(new Vector2(50, 50))); window.Orphan(); } /// /// Assert that resizable windows can be resized by dragging their bottom-right corner. /// [Test] public void TestWindowCanBeResizedByDragging() { _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 100, 100)); var window = new TestWindow { SetSize = new Vector2(40, 40), MinSize = new Vector2(10, 10), MouseFilter = Control.MouseFilterMode.Stop, }; _userInterfaceManager.WindowRoot.AddChild(window); window.Arrange(UIBox2.FromDimensions(new Vector2(20, 20), new Vector2(40, 40))); window.TestKeyBindDown(CreateGuiEvent(BoundKeyState.Down, new Vector2(59, 59), new Vector2(39, 39))); window.TestMouseMove(CreateMouseMoveEvent(window, new Vector2(79, 84))); window.TestKeyBindUp(CreateGuiEvent(BoundKeyState.Up, new Vector2(79, 84), new Vector2(59, 64))); Assert.Multiple(() => { Assert.That(window.Position, Is.EqualTo(new Vector2(20, 20))); Assert.That(window.SetSize, Is.EqualTo(new Vector2(60, 65))); }); window.Orphan(); } private static GUIBoundKeyEventArgs CreateGuiEvent( BoundKeyState state, Vector2 globalPosition, Vector2 relativePosition) { return new GUIBoundKeyEventArgs( EngineKeyFunctions.UIClick, state, new ScreenCoordinates(globalPosition, WindowId.Main), true, relativePosition, relativePosition); } private static GUIMouseMoveEventArgs CreateMouseMoveEvent(Control control, Vector2 globalPosition) { return new GUIMouseMoveEventArgs( Vector2.Zero, control, globalPosition, new ScreenCoordinates(globalPosition, WindowId.Main), globalPosition - control.Position, globalPosition - control.PixelPosition); } private sealed class TestWindow : BaseWindow { public void TestKeyBindDown(GUIBoundKeyEventArgs args) { KeyBindDown(args); } public void TestKeyBindUp(GUIBoundKeyEventArgs args) { KeyBindUp(args); } public void TestMouseMove(GUIMouseMoveEventArgs args) { MouseMove(args); } protected override DragMode GetDragModeFor(Vector2 relativeMousePos) { if (Resizable && relativeMousePos.X > Size.X - 5 && relativeMousePos.Y > Size.Y - 5) { return DragMode.Bottom | DragMode.Right; } if (relativeMousePos.Y < 20) return DragMode.Move; return DragMode.None; } } } }