using System; using System.Numerics; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Timing; namespace Robust.Client.UserInterface.CustomControls { /// /// Provides basic functionality for windows that can be opened, dragged around, etc... /// public abstract class BaseWindow : Control { private DragMode CurrentDrag = DragMode.None; private Vector2 DragOffsetTopLeft; private Vector2 DragOffsetBottomRight; private Control? _trackedParent; private Vector2? _lastParentSize; public bool Resizable { get; set; } = true; public bool IsOpen => Parent != null; /// /// Invoked when the close button of this window is pressed. /// public event Action? OnClose; public event Action? OnOpen; public virtual void Close() { if (Parent == null) { return; } Parent.RemoveChild(this); OnClose?.Invoke(); } protected override void EnteredTree() { base.EnteredTree(); TrackParentResize(); } protected override void ExitedTree() { UntrackParentResize(); base.ExitedTree(); } protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); if (args.Function != EngineKeyFunctions.UIClick) { return; } CurrentDrag = GetDragModeFor(args.RelativePosition); if (CurrentDrag != DragMode.None) { DragOffsetTopLeft = args.PointerLocation.Position / UIScale - Position; DragOffsetBottomRight = Position + Size - args.PointerLocation.Position / UIScale; } MoveToFront(); } protected internal override void KeyBindUp(GUIBoundKeyEventArgs args) { base.KeyBindUp(args); if (args.Function != EngineKeyFunctions.UIClick) { return; } DragOffsetTopLeft = DragOffsetBottomRight = Vector2.Zero; CurrentDrag = DragMode.None; // If this is done in MouseDown, Godot won't fire MouseUp as you need focus to receive MouseUps. UserInterfaceManager.KeyboardFocused?.ReleaseKeyboardFocus(); } protected internal override void MouseMove(GUIMouseMoveEventArgs args) { base.MouseMove(args); if (Parent == null) { return; } if (CurrentDrag == DragMode.Move) { var globalPos = args.GlobalPosition; globalPos = Vector2.Clamp(globalPos, Vector2.Zero, Parent.Size); LayoutContainer.SetPosition(this, globalPos - DragOffsetTopLeft); return; } if (!Resizable) { return; } if (CurrentDrag == DragMode.None) { var cursor = CursorShape.Arrow; var previewDragMode = GetDragModeFor(args.RelativePosition); previewDragMode &= ~DragMode.Move; switch (previewDragMode) { case DragMode.Top: case DragMode.Bottom: cursor = CursorShape.VResize; break; case DragMode.Left: case DragMode.Right: cursor = CursorShape.HResize; break; case DragMode.Bottom | DragMode.Left: case DragMode.Top | DragMode.Right: cursor = CursorShape.NESWResize; break; case DragMode.Bottom | DragMode.Right: case DragMode.Top | DragMode.Left: cursor = CursorShape.NWSEResize; break; } DefaultCursorShape = cursor; } else { var (left, top) = Position; var (right, bottom) = Position + SetSize; if (float.IsNaN(SetSize.X)) { right = Position.X + Size.X; } if (float.IsNaN(SetSize.Y)) { bottom = Position.Y + Size.Y; } if ((CurrentDrag & DragMode.Top) == DragMode.Top) { top = Math.Min(args.GlobalPosition.Y - DragOffsetTopLeft.Y, Math.Min(bottom, bottom - MinSize.Y)); } else if ((CurrentDrag & DragMode.Bottom) == DragMode.Bottom) { bottom = Math.Max(args.GlobalPosition.Y + DragOffsetBottomRight.Y, Math.Max(top, top + MinSize.Y)); } if ((CurrentDrag & DragMode.Left) == DragMode.Left) { left = Math.Min(args.GlobalPosition.X - DragOffsetTopLeft.X, Math.Min(right, right - MinSize.X)); } else if ((CurrentDrag & DragMode.Right) == DragMode.Right) { right = Math.Max(args.GlobalPosition.X + DragOffsetBottomRight.X, Math.Max(left, left + MinSize.X)); } var rect = new UIBox2(left, top, right, bottom); LayoutContainer.SetPosition(this, rect.TopLeft); SetSize = rect.Size; } } protected internal override void MouseExited() { base.MouseExited(); if (Resizable && CurrentDrag == DragMode.None) { DefaultCursorShape = CursorShape.Arrow; } } public void MoveToFront() { if (Parent == null) { throw new InvalidOperationException("This window is not currently open."); } SetPositionLast(); } public bool IsAtFront() { if (Parent == null) { throw new InvalidOperationException("This window is not currently open"); } var siblingCount = Parent.ChildCount; var ourPos = GetPositionInParent(); for (var i = ourPos + 1; i < siblingCount; i++) { if (Parent.GetChild(i).Visible) { // If we find a control after us that's visible, we're NOT in front. return false; } } return true; } public void Open() { if (!Visible) { Visible = true; Logger.WarningS("ui", $"Window {this} had visibility false. Do not use visibility on DefaultWindow."); } if (!IsOpen) { UserInterfaceManager.WindowRoot.AddChild(this); } Opened(); OnOpen?.Invoke(); } /// /// Opens the window and places it at the specified position. /// public void Open(Vector2 position) { Measure(Vector2Helpers.Infinity); Open(); LayoutContainer.SetPosition(this, position); } public void OpenCentered() => OpenCenteredAt(new Vector2(0.5f, 0.5f)); public void OpenToLeft() => OpenCenteredAt(new Vector2(0, 0.5f)); public void OpenCenteredLeft() => OpenCenteredAt(new Vector2(0.25f, 0.5f)); public void OpenToRight() => OpenCenteredAt(new Vector2(1, 0.5f)); public void OpenCenteredRight() => OpenCenteredAt(new Vector2(0.75f, 0.5f)); /// /// Opens a window and centers it relative to the screen position. /// public void OpenScreenAt(Vector2 relativePosition, IClyde clyde) { var adjusted = relativePosition / clyde.ScreenSize; OpenCenteredAt(adjusted); } /// /// Opens a window, attempting to place the center of the window at some relative point on the screen. /// /// Fractional screen position. So (0,0) is the upper left, and (1,1) is the /// lower right. public void OpenCenteredAt(Vector2 relativePosition) { Measure(Vector2Helpers.Infinity); Open(); RecenterWindow(relativePosition); } /// /// Repositions a window, attempting to place the center of the window at some relative point on the screen. /// /// Fractional screen position. So (0,0) is the upper left, and (1,1) is the /// lower right. public void RecenterWindow(Vector2 relativePosition) { if (Parent == null) return; // Where we want the upper left corner of the window to be var corner = Parent!.Size * Vector2.Clamp(relativePosition, Vector2.Zero, Vector2.One) - DesiredSize / 2; // Attempt to keep the whole window is visible, regardless of the target position. e.g., if the target for // the center is (0,0), this will actually open the window so that the upper left is at (0,0). If the window // is bigger than the parent, this will currently prioritize showing the upper left corner. var pos = Vector2.Clamp(corner, Vector2.Zero, Parent.Size - DesiredSize); LayoutContainer.SetPosition(this, pos); } protected virtual void Opened() { } protected virtual DragMode GetDragModeFor(Vector2 relativeMousePos) { return DragMode.None; } private void TrackParentResize() { if (Parent == null) return; _trackedParent = Parent; _trackedParent.OnResized += ParentResized; _lastParentSize = _trackedParent.Size; } private void UntrackParentResize() { if (_trackedParent != null) _trackedParent.OnResized -= ParentResized; _trackedParent = null; _lastParentSize = null; } /// /// Re-locates this window if the parent re-sizes. Useful if display size changes. /// private void ParentResized() { // Sanity check if (_trackedParent == null || Parent != _trackedParent) return; var newParentSize = _trackedParent.Size; if (_lastParentSize is not { } oldParentSize || oldParentSize.X <= 0 || oldParentSize.Y <= 0 || newParentSize.X <= 0 || newParentSize.Y <= 0) { _lastParentSize = newParentSize; return; } if (oldParentSize.EqualsApprox(newParentSize)) return; var relativeCenter = (Position + Size / 2) / oldParentSize; var newPosition = relativeCenter * newParentSize - Size / 2; var maxPosition = Vector2.Max(newParentSize - Size, Vector2.Zero); _lastParentSize = newParentSize; LayoutContainer.SetPosition(this, Vector2.Clamp(newPosition, Vector2.Zero, maxPosition)); } [Flags] protected enum DragMode : byte { None = 0, Move = 1, Top = 1 << 1, Bottom = 1 << 2, Left = 1 << 3, Right = 1 << 4, } } }