using System; using JetBrains.Annotations; using Robust.Shared.Maths; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface { public partial class Control { public event Action OnMinimumSizeChanged; private Vector2 _size; private float _sizeFlagsStretchRatio = 1; private Vector2? _calculatedMinimumSize; private Vector2 _customMinimumSize; private SizeFlags _sizeFlagsHorizontal = SizeFlags.Fill; private SizeFlags _sizeFlagsVertical = SizeFlags.Fill; private bool _layoutDirty; /// /// 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; internal set { if (_size == value) { return; } _size = value; Resized(); UpdateLayout(); } } /// /// 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; internal set; } /// /// 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); /// /// Horizontal size flags for container layout. /// [ViewVariables] public SizeFlags SizeFlagsHorizontal { get => _sizeFlagsHorizontal; set { _sizeFlagsHorizontal = value; Parent?.UpdateLayout(); } } /// /// Vertical size flags for container layout. /// [ViewVariables] public SizeFlags SizeFlagsVertical { get => _sizeFlagsVertical; set { _sizeFlagsVertical = value; Parent?.UpdateLayout(); } } /// /// 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; Parent?.UpdateLayout(); } } /// /// 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() { var min = Vector2.Zero; foreach (var child in Children) { min = Vector2.ComponentMax(min, child.CombinedMinimumSize); } return min; } /// /// 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); Parent?.MinimumSizeChanged(); 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(); foreach (var child in Children) { child.ForceRunLayoutUpdate(); } } protected void UpdateLayout() { if (_layoutDirty) { // Already queued for a layout update, don't bother. return; } _layoutDirty = true; UserInterfaceManagerInternal.QueueLayoutUpdate(this); } protected void FitChildInPixelBox(Control child, UIBox2i pixelBox) { var topLeft = pixelBox.TopLeft / UIScale; var bottomRight = pixelBox.BottomRight / UIScale; FitChildInBox(child, new UIBox2(topLeft, bottomRight)); } protected void FitChildInBox(Control child, UIBox2 box) { DebugTools.Assert(child.Parent == this); var (minX, minY) = child.CombinedMinimumSize; var newPosX = box.Left; var newSizeX = minX; if ((child.SizeFlagsHorizontal & SizeFlags.ShrinkEnd) != 0) { newPosX += (box.Width - minX); } else if ((child.SizeFlagsHorizontal & SizeFlags.ShrinkCenter) != 0) { newPosX += (box.Width - minX) / 2; } else if ((child.SizeFlagsHorizontal & SizeFlags.Fill) != 0) { newSizeX = box.Width; } var newPosY = box.Top; var newSizeY = minY; if ((child.SizeFlagsVertical & SizeFlags.ShrinkEnd) != 0) { newPosY += (box.Height - minY); } else if ((child.SizeFlagsVertical & SizeFlags.ShrinkCenter) != 0) { newPosY += (box.Height - minY) / 2; } else if ((child.SizeFlagsVertical & SizeFlags.Fill) != 0) { newSizeY = box.Height; } child.Position = new Vector2(newPosX, newPosY); child.Size = new Vector2(newSizeX, newSizeY); } internal void DoLayoutUpdate() { LayoutUpdateOverride(); _layoutDirty = false; } protected virtual void LayoutUpdateOverride() { foreach (var child in Children) { FitChildInPixelBox(child, PixelSizeBox); } } /// /// 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, } } }