using System; using System.Collections; using System.Collections.Generic; using System.Numerics; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Maths; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Client.UserInterface.CustomControls { /// /// Simple window implementation that can be resized and has a title bar. /// /// /// Warning: ugly. /// [GenerateTypedNameReferences] [Virtual] // ReSharper disable once InconsistentNaming public partial class DefaultWindow : BaseWindow { public const string StyleClassWindowTitle = "windowTitle"; public const string StyleClassWindowPanel = "windowPanel"; public const string StyleClassWindowHeader = "windowHeader"; public const string StyleClassWindowCloseButton = "windowCloseButton"; /// /// This ensures that windows are always visible on screen and don't become unreachable. This only applies /// if the window is allowed to be partially off-screen. /// public float WindowEdgeSeparation = 30; /// /// If a window is completely off-screen, the window will be bumped away from the edge so that its not a /// tiny square hidden in a corner. Effectively a conditional increase to . /// Helpful when changing UI scale or changing the window size. /// public float WindowEdgeBump = 50; /// /// This option determines whether a window is allowed to be dragged off the edge of the screen (as long as /// part of it remains visible). /// /// /// Note that you generally want to disable north, as that will mean the header might not be visible, which /// might render the window unmovable. /// public DirectionFlag AllowOffScreen = ~DirectionFlag.North; private string? _headerClass; private string? _titleClass; public DefaultWindow() { RobustXamlLoader.Load(this); MouseFilter = MouseFilterMode.Stop; WindowHeader.MinSize = new Vector2(0, HEADER_SIZE_Y); Contents = ContentsContainer; XamlChildren = new SS14ContentCollection(this); } public string? HeaderClass { get => _headerClass; set { if (_headerClass == value) return; if (_headerClass != null) WindowHeader.RemoveStyleClass(_headerClass); if (value != null) WindowHeader.AddStyleClass(value); _headerClass = value; } } public string? TitleClass { get => _titleClass; set { if (_titleClass == value) return; if (_titleClass != null) TitleLabel.RemoveStyleClass(_titleClass); if (value != null) TitleLabel.AddStyleClass(value); _titleClass = value; } } public Control Contents { get; private set; } private const int DRAG_MARGIN_SIZE = 7; // TODO: Un-hard code this header size. private const float HEADER_SIZE_Y = 25; protected virtual Vector2 ContentsMinimumSize => new Vector2(50, 50); protected override Vector2 MeasureOverride(Vector2 availableSize) { return Vector2.Max( ContentsMinimumSize, base.MeasureOverride(Vector2.Max(availableSize, ContentsMinimumSize))); } public string? Title { get => TitleLabel.Text; set => TitleLabel.Text = value; } // Drag resizing and moving code is mostly taken from Godot's WindowDialog. protected override void EnteredTree() { base.EnteredTree(); CloseButton.OnPressed += CloseButtonPressed; } protected override void ExitedTree() { CloseButton.OnPressed -= CloseButtonPressed; base.ExitedTree(); } private void CloseButtonPressed(BaseButton.ButtonEventArgs args) { Close(); } // Prevent window headers from getting off screen due to game window resizes. protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); // This is to avoid unnecessarily setting a position where our size isn't yet fully updated. // This most commonly happens with saved window positions if your window position is <= 0. if (!IsMeasureValid) return; var (spaceX, spaceY) = Parent!.Size; var maxX = spaceX - ((AllowOffScreen & DirectionFlag.West) == 0 ? Size.X : WindowEdgeSeparation); var maxY = spaceY - ((AllowOffScreen & DirectionFlag.South) == 0 ? Size.Y : WindowEdgeSeparation); if (Position.X > spaceX) maxX -= WindowEdgeBump; if (Position.Y > spaceY) maxY -= WindowEdgeBump; var pos = Vector2.Min(Position, new Vector2(maxX, maxY)); var minX = (AllowOffScreen & DirectionFlag.East) == 0 ? 0 : WindowEdgeSeparation - Size.X; var minY = (AllowOffScreen & DirectionFlag.North) == 0 ? 0 : WindowEdgeSeparation - Size.Y; pos = Vector2.Max(pos, new Vector2(minX, minY)); if (Position != pos) LayoutContainer.SetPosition(this, pos); } protected override DragMode GetDragModeFor(Vector2 relativeMousePos) { var mode = DragMode.None; if (Resizable) { if (relativeMousePos.Y < DRAG_MARGIN_SIZE) { mode = DragMode.Top; } else if (relativeMousePos.Y > Size.Y - DRAG_MARGIN_SIZE) { mode = DragMode.Bottom; } if (relativeMousePos.X < DRAG_MARGIN_SIZE) { mode |= DragMode.Left; } else if (relativeMousePos.X > Size.X - DRAG_MARGIN_SIZE) { mode |= DragMode.Right; } } if (mode == DragMode.None && relativeMousePos.Y < HEADER_SIZE_Y) { mode = DragMode.Move; } return mode; } public sealed class SS14ContentCollection : ICollection, IReadOnlyCollection { private readonly DefaultWindow Owner; public SS14ContentCollection(DefaultWindow owner) { Owner = owner; } public Enumerator GetEnumerator() { return new(Owner); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Add(Control item) { Owner.Contents.AddChild(item); } public void Clear() { Owner.Contents.RemoveAllChildren(); } public bool Contains(Control item) { return item?.Parent == Owner.Contents; } public void CopyTo(Control[] array, int arrayIndex) { Owner.Contents.Children.CopyTo(array, arrayIndex); } public bool Remove(Control item) { if (item?.Parent != Owner.Contents) { return false; } DebugTools.AssertNotNull(Owner?.Contents); Owner!.Contents.RemoveChild(item); return true; } int ICollection.Count => Owner.Contents.ChildCount; int IReadOnlyCollection.Count => Owner.Contents.ChildCount; public bool IsReadOnly => false; public struct Enumerator : IEnumerator { private OrderedChildCollection.Enumerator _enumerator; internal Enumerator(DefaultWindow DefaultWindow) { _enumerator = DefaultWindow.Contents.Children.GetEnumerator(); } public bool MoveNext() { return _enumerator.MoveNext(); } public void Reset() { _enumerator.Reset(); } public Control Current => _enumerator.Current; object IEnumerator.Current => Current; public void Dispose() { _enumerator.Dispose(); } } } } }