using System; using JetBrains.Annotations; using Robust.Client.UserInterface.Controls; using Robust.Shared.Maths; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface { public partial class Control { private float _anchorBottom; private float _anchorLeft; private float _anchorRight; private float _anchorTop; private float _marginRight; private float _marginLeft; private float _marginTop; private float _marginBottom; private Vector2 _position; private Vector2 _sizeByMargins; private Vector2 _size; private float _sizeFlagsStretchRatio = 1; private Vector2? _calculatedMinimumSize; private Vector2 _customMinimumSize; private GrowDirection _growHorizontal; private GrowDirection _growVertical; public event Action OnMinimumSizeChanged; private SizeFlags _sizeFlagsHorizontal = SizeFlags.Fill; private SizeFlags _sizeFlagsVertical = SizeFlags.Fill; private bool _layoutDirty; /// /// The value of an anchor that is exactly on the begin of the parent control. /// public const float AnchorBegin = 0; /// /// The value of an anchor that is exactly on the end of the parent control. /// public const float AnchorEnd = 1; /// /// Specifies the anchor of the bottom edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float AnchorBottom { get => _anchorBottom; set { _anchorBottom = value; DoLayoutUpdate(); } } /// /// Specifies the anchor of the left edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float AnchorLeft { get => _anchorLeft; set { _anchorLeft = value; DoLayoutUpdate(); } } /// /// Specifies the anchor of the right edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float AnchorRight { get => _anchorRight; set { _anchorRight = value; DoLayoutUpdate(); } } /// /// Specifies the anchor of the top edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float AnchorTop { get => _anchorTop; set { _anchorTop = value; DoLayoutUpdate(); } } /// /// Specifies the margin of the right edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float MarginRight { get => _marginRight; set { _marginRight = value; DoLayoutUpdate(); } } /// /// Specifies the margin of the left edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float MarginLeft { get => _marginLeft; set { _marginLeft = value; DoLayoutUpdate(); } } /// /// Specifies the margin of the top edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float MarginTop { get => _marginTop; set { _marginTop = value; DoLayoutUpdate(); } } /// /// Specifies the margin of the bottom edge of the control. /// [ViewVariables(VVAccess.ReadWrite)] public float MarginBottom { get => _marginBottom; set { _marginBottom = value; DoLayoutUpdate(); } } /// /// Called when the for this control changes. /// protected internal virtual void UIScaleChanged() { MinimumSizeChanged(); } /// /// The amount of "real" pixels a virtual pixel takes up. /// The higher the number, the bigger the interface. /// [ViewVariables] protected float UIScale => UserInterfaceManager.UIScale; /// /// The size of this control, in virtual pixels. /// /// /// /// [ViewVariables(VVAccess.ReadWrite)] public Vector2 Size { get => _size; set { var (diffX, diffY) = value - _sizeByMargins; _marginRight += diffX; _marginBottom += diffY; DoLayoutUpdate(); } } /// /// The size of this control, in physical pixels. /// [ViewVariables] public Vector2i PixelSize => (Vector2i) (_size * UserInterfaceManager.UIScale); /// /// A with the top left at 0,0 and the size equal to . /// /// public UIBox2 SizeBox => new UIBox2(Vector2.Zero, Size); /// /// A with the top left at 0,0 and the size equal to . /// /// public UIBox2i PixelSizeBox => new UIBox2i(Vector2i.Zero, PixelSize); /// /// The width of the control, in virtual pixels. /// /// public float Width => Size.X; /// /// The height of the control, in virtual pixels. /// /// public float Height => Size.Y; /// /// The width of the control, in physical pixels. /// /// public int PixelWidth => PixelSize.X; /// /// The height of the control, in physical pixels. /// /// public int PixelHeight => PixelSize.Y; /// /// The position of the top left corner of the control, in virtual pixels. /// This is relative to the position of the parent. /// /// /// [ViewVariables(VVAccess.ReadWrite)] public Vector2 Position { get => _position; set { var (diffX, diffY) = value - _position; _marginTop += diffY; _marginBottom += diffY; _marginLeft += diffX; _marginRight += diffX; DoLayoutUpdate(); } } /// /// The position of the top left corner of the control, in physical pixels. /// /// [ViewVariables] public Vector2i PixelPosition => (Vector2i) (_position * UserInterfaceManager.UIScale); /// /// The position of the top left corner of the control, in virtual pixels. /// This is not relative to the parent. /// /// /// [ViewVariables] public Vector2 GlobalPosition { get { var offset = Position; var parent = Parent; while (parent != null) { offset += parent.Position; parent = parent.Parent; } return offset; } } /// /// The position of the top left corner of the control, in physical pixels. /// This is not relative to the parent. /// /// [ViewVariables] public Vector2i GlobalPixelPosition { get { var offset = PixelPosition; var parent = Parent; while (parent != null) { offset += parent.PixelPosition; parent = parent.Parent; } return offset; } } /// /// Represents the "rectangle" of the control relative to the parent, in virtual pixels. /// /// public UIBox2 Rect => UIBox2.FromDimensions(_position, _size); /// /// Represents the "rectangle" of the control relative to the parent, in physical pixels. /// /// public UIBox2i PixelRect => UIBox2i.FromDimensions(PixelPosition, PixelSize); /// /// Determines how the control will move on the horizontal axis to ensure it is at its minimum size. /// See for more information. /// [ViewVariables] public GrowDirection GrowHorizontal { get => _growHorizontal; set { _growHorizontal = value; UpdateLayout(); } } /// /// Determines how the control will move on the vertical axis to ensure it is at its minimum size. /// See for more information. /// [ViewVariables] public GrowDirection GrowVertical { get => _growVertical; set { _growVertical = value; UpdateLayout(); } } /// /// Horizontal size flags for container layout. /// [ViewVariables] public SizeFlags SizeFlagsHorizontal { get => _sizeFlagsHorizontal; set { _sizeFlagsHorizontal = value; if (Parent is Container container) { container.QueueSortChildren(); } } } /// /// Vertical size flags for container layout. /// [ViewVariables] public SizeFlags SizeFlagsVertical { get => _sizeFlagsVertical; set { _sizeFlagsVertical = value; if (Parent is Container container) { container.QueueSortChildren(); } } } /// /// Stretch ratio used to give shared of the available space in case multiple siblings are set to expand /// in a container /// /// /// Thrown if the value is less than or equal to 0. /// [ViewVariables] public float SizeFlagsStretchRatio { get => _sizeFlagsStretchRatio; set { if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero."); } _sizeFlagsStretchRatio = value; if (Parent is Container container) { container.QueueSortChildren(); } } } /// /// A combination of and , /// Whichever is greater. /// Use this for whenever you need the *actual* minimum size of something. /// /// /// This is in virtual pixels. /// /// [ViewVariables] public Vector2 CombinedMinimumSize { get { if (!_calculatedMinimumSize.HasValue) { _updateMinimumSize(); DebugTools.Assert(_calculatedMinimumSize.HasValue); } return Vector2.ComponentMax(CustomMinimumSize, _calculatedMinimumSize.Value); } } /// /// The , in physical pixels. /// public Vector2i CombinedPixelMinimumSize => (Vector2i) (CombinedMinimumSize * UIScale); /// /// A custom minimum size. If the control-calculated size is is smaller than this, this is used instead. /// /// /// [ViewVariables] public Vector2 CustomMinimumSize { get => _customMinimumSize; set { _customMinimumSize = Vector2.ComponentMax(Vector2.Zero, value); MinimumSizeChanged(); } } private void _updateMinimumSize() { if (_stylingDirty) { ForceRunStyleUpdate(); } _calculatedMinimumSize = Vector2.ComponentMax(Vector2.Zero, CalculateMinimumSize()); } /// /// Override this to calculate a minimum size for this control. /// Do NOT call this directly to get the minimum size for layout purposes! /// Use for the ACTUAL minimum size. /// protected virtual Vector2 CalculateMinimumSize() { return Vector2.Zero; } /// /// Tells the GUI system that the minimum size of this control may have changed, /// so that say containers will re-sort it if necessary. /// public void MinimumSizeChanged() { _calculatedMinimumSize = null; OnMinimumSizeChanged?.Invoke(this); // TODO: this is a terrible hack. if (!(Parent is Container)) { UpdateLayout(); } } /// /// Sets an anchor AND a margin preset. This is most likely the method you want. /// public void SetAnchorAndMarginPreset(LayoutPreset preset, LayoutPresetMode mode = LayoutPresetMode.MinSize, int margin = 0) { SetAnchorPreset(preset); SetMarginsPreset(preset, mode, margin); } /// /// Changes all the anchors of a node at once to common presets. /// The result is that the anchors are laid out to be suitable for a preset. /// /// /// The preset to apply to the anchors. /// /// /// If this is true, the control margin values themselves will not be changed, /// and the control position and size will change according to the new anchor parameters. /// If false, the control margins will adjust so that the control position and size remains the same relative to its parent. /// /// /// Thrown if isn't a valid preset value. /// public void SetAnchorPreset(LayoutPreset preset, bool keepMargin = false) { // TODO: Implement keepMargin. // Left Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.BottomLeft: case LayoutPreset.CenterLeft: case LayoutPreset.LeftWide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Wide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: AnchorLeft = 0; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: AnchorLeft = 0.5f; break; case LayoutPreset.TopRight: case LayoutPreset.BottomRight: case LayoutPreset.CenterRight: case LayoutPreset.RightWide: AnchorLeft = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Top Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.LeftWide: case LayoutPreset.TopWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.CenterTop: case LayoutPreset.VerticalCenterWide: AnchorTop = 0; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Center: AnchorTop = 0.5f; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.BottomWide: AnchorTop = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Right Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.CenterLeft: case LayoutPreset.BottomLeft: case LayoutPreset.LeftWide: AnchorRight = 0; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: AnchorRight = 0.5f; break; case LayoutPreset.CenterRight: case LayoutPreset.TopRight: case LayoutPreset.Wide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: case LayoutPreset.RightWide: case LayoutPreset.BottomRight: AnchorRight = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Bottom Anchor. switch (preset) { case LayoutPreset.TopWide: case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.CenterTop: AnchorBottom = 0; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.Center: case LayoutPreset.HorizontalCenterWide: AnchorBottom = 0.5f; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.LeftWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.VerticalCenterWide: case LayoutPreset.BottomWide: AnchorBottom = 1; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } } /// /// Changes all the margins of a control at once to common presets. /// The result is that the control is laid out as specified by the preset. /// /// /// /// Some extra margin to add depending on the preset chosen. public void SetMarginsPreset(LayoutPreset preset, LayoutPresetMode resizeMode = LayoutPresetMode.MinSize, int margin = 0) { var newSize = Size; var minSize = CombinedMinimumSize; if ((resizeMode & LayoutPresetMode.KeepWidth) == 0) { newSize = new Vector2(minSize.X, newSize.Y); } if ((resizeMode & LayoutPresetMode.KeepHeight) == 0) { newSize = new Vector2(newSize.X, minSize.Y); } var parentSize = Parent?.Size ?? Vector2.Zero; // Left Margin. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.BottomLeft: case LayoutPreset.CenterLeft: case LayoutPreset.LeftWide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Wide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: // The AnchorLeft bit is to reverse the effect of anchors, // So that the preset result is the same no matter what margins are set. _marginLeft = parentSize.X * (0 - AnchorLeft) + margin; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: _marginLeft = parentSize.X * (0.5f - AnchorLeft) - newSize.X / 2; break; case LayoutPreset.TopRight: case LayoutPreset.BottomRight: case LayoutPreset.CenterRight: case LayoutPreset.RightWide: _marginLeft = parentSize.X * (1 - AnchorLeft) - newSize.X - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Top Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.LeftWide: case LayoutPreset.TopWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.CenterTop: case LayoutPreset.VerticalCenterWide: _marginTop = parentSize.Y * (0 - AnchorTop) + margin; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.Center: _marginTop = parentSize.Y * (0.5f - AnchorTop) - newSize.Y / 2; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.BottomWide: _marginTop = parentSize.Y * (1 - AnchorTop) - newSize.Y - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Right Anchor. switch (preset) { case LayoutPreset.TopLeft: case LayoutPreset.CenterLeft: case LayoutPreset.BottomLeft: case LayoutPreset.LeftWide: _marginRight = parentSize.X * (0 - AnchorRight) + newSize.X + margin; break; case LayoutPreset.CenterTop: case LayoutPreset.CenterBottom: case LayoutPreset.Center: case LayoutPreset.VerticalCenterWide: _marginRight = parentSize.X * (0.5f - AnchorRight) + newSize.X; break; case LayoutPreset.CenterRight: case LayoutPreset.TopRight: case LayoutPreset.Wide: case LayoutPreset.HorizontalCenterWide: case LayoutPreset.TopWide: case LayoutPreset.BottomWide: case LayoutPreset.RightWide: case LayoutPreset.BottomRight: _marginRight = parentSize.X * (1 - AnchorRight) - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } // Bottom Anchor. switch (preset) { case LayoutPreset.TopWide: case LayoutPreset.TopLeft: case LayoutPreset.TopRight: case LayoutPreset.CenterTop: _marginBottom = parentSize.Y * (0 - AnchorBottom) + newSize.Y + margin; break; case LayoutPreset.CenterLeft: case LayoutPreset.CenterRight: case LayoutPreset.Center: case LayoutPreset.HorizontalCenterWide: _marginBottom = parentSize.Y * (0.5f - AnchorBottom) + newSize.Y; break; case LayoutPreset.CenterBottom: case LayoutPreset.BottomLeft: case LayoutPreset.BottomRight: case LayoutPreset.LeftWide: case LayoutPreset.Wide: case LayoutPreset.RightWide: case LayoutPreset.VerticalCenterWide: case LayoutPreset.BottomWide: _marginBottom = parentSize.Y * (1 - AnchorBottom) - margin; break; default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } UpdateLayout(); } /// /// Forces this component to immediately calculate layout. /// /// /// This should only be used for unit testing, /// where running the deferred layout updating system in the UI manager can be annoying. /// If you are forced to use this in regular code, you have found a bug. /// public void ForceRunLayoutUpdate() { DoLayoutUpdate(); // I already apologize, Sonar. // This is terrible. if (this is Container container) { container.SortChildren(); } foreach (var child in Children) { child.ForceRunLayoutUpdate(); } } public enum LayoutPreset : byte { TopLeft = 0, TopRight = 1, BottomLeft = 2, BottomRight = 3, CenterLeft = 4, CenterTop = 5, CenterRight = 6, CenterBottom = 7, Center = 8, LeftWide = 9, TopWide = 10, RightWide = 11, BottomWide = 12, VerticalCenterWide = 13, HorizontalCenterWide = 14, Wide = 15, } /// [Flags] [PublicAPI] public enum LayoutPresetMode : byte { /// /// Reset control size to minimum size. /// MinSize = 0, /// /// Reset height to minimum but keep width the same. /// KeepWidth = 1, /// /// Reset width to minimum but keep height the same. /// KeepHeight = 2, /// /// Do not modify control size at all. /// KeepSize = KeepWidth | KeepHeight, } /// /// Controls how a control changes size when inside a container. /// [Flags] [PublicAPI] public enum SizeFlags : byte { /// /// Shrink to the begin of the specified axis. /// None = 0, /// /// Fill as much space as possible in a container, without pushing others. /// Fill = 1, /// /// Fill as much space as possible in a container, pushing other nodes. /// The ratio of pushing if there's multiple set to expand is dependant on /// Expand = 2, /// /// Combination of and . /// FillExpand = 3, /// /// Shrink inside a container, aligning to the center. /// ShrinkCenter = 4, /// /// Shrink inside a container, aligning to the end. /// ShrinkEnd = 8, } /// /// Controls how the control should move when its wanted size (controlled by anchors/margins) is smaller /// than its minimum size. /// public enum GrowDirection : byte { /// /// The control will expand to the bottom right to reach its minimum size. /// End = 0, /// /// The control will expand to the top left to reach its minimum size. /// Begin, /// /// The control will expand on all axes equally to reach its minimum size. /// Both } private protected void UpdateLayout() { if (_layoutDirty) { // Already queued for a layout update, don't bother. return; } _layoutDirty = true; UserInterfaceManagerInternal.QueueLayoutUpdate(this); } internal void DoLayoutUpdate() { _layoutDirty = false; var (pSizeX, pSizeY) = Parent?._size ?? Vector2.Zero; // Calculate where the control "wants" to be by its anchors/margins. var top = _anchorTop * pSizeY + _marginTop; var left = _anchorLeft * pSizeX + _marginLeft; var right = _anchorRight * pSizeX + _marginRight; var bottom = _anchorBottom * pSizeY + _marginBottom; // The position we want. var (wPosX, wPosY) = (left, top); // The size we want. var (wSizeX, wSizeY) = (right - left, bottom - top); var (minSizeX, minSizeY) = CombinedMinimumSize; _handleLayoutOverflow(GrowHorizontal, minSizeX, wPosX, wSizeX, out var posX, out var sizeX); _handleLayoutOverflow(GrowVertical, minSizeY, wPosY, wSizeY, out var posY, out var sizeY); var oldSize = _size; _position = (posX, posY); _size = (sizeX, sizeY); _sizeByMargins = (wSizeX, wSizeY); // If size is different then child controls may need to be laid out differently. if (_size != oldSize) { Resized(); foreach (var child in _orderedChildren) { child.UpdateLayout(); } } } private static void _handleLayoutOverflow(GrowDirection direction, float minSize, float wPos, float wSize, out float pos, out float size) { var overflow = minSize - wSize; if (overflow <= 0) { pos = wPos; size = wSize; return; } switch (direction) { case GrowDirection.End: pos = wPos; break; case GrowDirection.Begin: pos = wPos - overflow; break; case GrowDirection.Both: pos = wPos - overflow / 2; break; default: throw new ArgumentOutOfRangeException(); } size = minSize; } } }