using System; using JetBrains.Annotations; using Robust.Shared.Maths; using Robust.Shared.ViewVariables; namespace Robust.Client.UserInterface { // Code and design heavily inspired by WPF/Avalonia. public partial class Control { private Vector2 _size; [ViewVariables] internal Vector2? PreviousMeasure; [ViewVariables] internal UIBox2? PreviousArrange; private float _sizeFlagsStretchRatio = 1; private float _minWidth; private float _minHeight; private float _setWidth = float.NaN; private float _setHeight = float.NaN; private float _maxWidth = float.PositiveInfinity; private float _maxHeight = float.PositiveInfinity; private bool _horizontalExpand; private bool _verticalExpand; private HAlignment _horizontalAlignment; private VAlignment _verticalAlignment; private Thickness _margin; private bool _isLayoutUpdateOverrideUsed; private bool _measuring; [ViewVariables] public Vector2 DesiredSize { get; private set; } [ViewVariables] public Vector2i DesiredPixelSize => (Vector2i) (DesiredSize * UIScale); [ViewVariables] public bool IsMeasureValid { get; private set; } [ViewVariables] public bool IsArrangeValid { get; private set; } [ViewVariables] public Thickness Margin { get => _margin; set { _margin = value; InvalidateMeasure(); } } /// /// Called when the for this control changes. /// protected internal virtual void UIScaleChanged() { InvalidateMeasure(); } /// /// The amount of "real" pixels a virtual pixel takes up. /// The higher the number, the bigger the interface. /// I.e. UIScale units are real pixels (rp) / virtual pixels (vp), /// real pixels varies depending on interface, virtual pixels doesn't. /// And vp * UIScale = rp, and rp / UIScale = vp /// [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(); } } /// /// 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(Vector2.Zero, Size); /// /// A with the top left at 0,0 and the size equal to . /// /// public UIBox2i PixelSizeBox => new(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] [Obsolete("Use HorizontalAlignment and HorizontalExpand instead.")] public SizeFlags SizeFlagsHorizontal { get { var flags = HorizontalAlignment switch { HAlignment.Stretch => SizeFlags.Fill, HAlignment.Left => SizeFlags.None, HAlignment.Center => SizeFlags.ShrinkCenter, HAlignment.Right => SizeFlags.ShrinkEnd, _ => throw new ArgumentOutOfRangeException() }; if (_horizontalExpand) flags |= SizeFlags.Expand; return flags; } set { HorizontalExpand = (value & SizeFlags.Expand) != 0; HorizontalAlignment = (value & ~SizeFlags.Expand) switch { SizeFlags.None => HAlignment.Left, SizeFlags.Fill => HAlignment.Stretch, SizeFlags.ShrinkCenter => HAlignment.Center, SizeFlags.ShrinkEnd => HAlignment.Right, _ => throw new ArgumentOutOfRangeException() }; Parent?.UpdateLayout(); } } /// /// Vertical size flags for container layout. /// [Obsolete("Use VerticalAlignment and VerticalExpand instead.")] [ViewVariables] public SizeFlags SizeFlagsVertical { get { var flags = _verticalAlignment switch { VAlignment.Stretch => SizeFlags.Fill, VAlignment.Top => SizeFlags.None, VAlignment.Center => SizeFlags.ShrinkCenter, VAlignment.Bottom => SizeFlags.ShrinkEnd, _ => throw new ArgumentOutOfRangeException() }; if (_verticalExpand) flags |= SizeFlags.Expand; return flags; } set { VerticalExpand = (value & SizeFlags.Expand) != 0; VerticalAlignment = (value & ~SizeFlags.Expand) switch { SizeFlags.None => VAlignment.Top, SizeFlags.Fill => VAlignment.Stretch, SizeFlags.ShrinkCenter => VAlignment.Center, SizeFlags.ShrinkEnd => VAlignment.Bottom, _ => throw new ArgumentOutOfRangeException() }; Parent?.UpdateLayout(); } } [ViewVariables(VVAccess.ReadWrite)] public HAlignment HorizontalAlignment { get => _horizontalAlignment; set { _horizontalAlignment = value; InvalidateArrange(); } } [ViewVariables(VVAccess.ReadWrite)] public VAlignment VerticalAlignment { get => _verticalAlignment; set { _verticalAlignment = value; InvalidateArrange(); } } [ViewVariables(VVAccess.ReadWrite)] public bool HorizontalExpand { get => _horizontalExpand; set { _horizontalExpand = value; Parent?.InvalidateMeasure(); } } [ViewVariables(VVAccess.ReadWrite)] public bool VerticalExpand { get => _verticalExpand; set { _verticalExpand = value; Parent?.InvalidateArrange(); } } /// /// 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?.InvalidateArrange(); } } /// /// A combination of and , /// Whichever is greater. /// Use this for whenever you need the *actual* minimum size of something. /// /// /// This is in virtual pixels. /// /// [Obsolete("Use DesiredSize and Measure()")] public Vector2 CombinedMinimumSize => DesiredSize; /// /// The , in physical pixels. /// [Obsolete("Use DesiredSize and Measure()")] 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] [Obsolete("Use MinSize instead.")] public Vector2 CustomMinimumSize { get => (_minWidth, _minHeight); set => (MinWidth, MinHeight) = Vector2.ComponentMax(Vector2.Zero, value); } public Vector2 MinSize { get => (_minWidth, _minHeight); set => (MinWidth, MinHeight) = Vector2.ComponentMax(Vector2.Zero, value); } public Vector2 SetSize { get => (_setWidth, _setHeight); set => (SetWidth, SetHeight) = value; } public Vector2 MaxSize { get => (_maxWidth, _maxHeight); set => (MaxWidth, MaxHeight) = value; } [ViewVariables(VVAccess.ReadWrite)] public float MinWidth { get => _minWidth; set { _minWidth = value; InvalidateMeasure(); } } [ViewVariables(VVAccess.ReadWrite)] public float MinHeight { get => _minHeight; set { _minHeight = value; InvalidateMeasure(); } } [ViewVariables(VVAccess.ReadWrite)] public float SetWidth { get => _setWidth; set { _setWidth = value; InvalidateMeasure(); } } [ViewVariables(VVAccess.ReadWrite)] public float SetHeight { get => _setHeight; set { _setHeight = value; InvalidateMeasure(); } } [ViewVariables(VVAccess.ReadWrite)] public float MaxWidth { get => _maxWidth; set { _maxWidth = value; InvalidateMeasure(); } } [ViewVariables(VVAccess.ReadWrite)] public float MaxHeight { get => _maxHeight; set { _maxHeight = value; InvalidateMeasure(); } } /// /// 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. /// [Obsolete("Implement MeasureOverride instead")] 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. /// [Obsolete("Use InvalidateMeasure()")] public void MinimumSizeChanged() { InvalidateMeasure(); } public void InvalidateMeasure() { if (!IsMeasureValid) return; IsMeasureValid = false; IsArrangeValid = false; UserInterfaceManagerInternal.QueueMeasureUpdate(this); } /// /// 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. /// [Obsolete("Call Arrange manually for unit tests or call Measure manually for early measures.")] public void ForceRunLayoutUpdate() { // TODO: Fix or remove this if (PreviousArrange.HasValue) Arrange(PreviousArrange.Value); } public void InvalidateArrange() { if (!IsArrangeValid) { // Already queued for a layout update, don't bother. return; } IsArrangeValid = false; UserInterfaceManagerInternal.QueueArrangeUpdate(this); } [Obsolete("Use InvalidateArrange()")] protected void UpdateLayout() { InvalidateArrange(); } public void Measure(Vector2 availableSize) { if (!IsMeasureValid || PreviousMeasure != availableSize) { IsMeasureValid = true; var desired = MeasureCore(availableSize); if (desired.X < 0 || desired.Y < 0 || !float.IsFinite(desired.X) || !float.IsFinite(desired.Y)) throw new InvalidOperationException("Invalid size returned from Measure()"); var prev = DesiredSize; DesiredSize = desired; PreviousMeasure = availableSize; if (prev != desired && Parent != null && !Parent._measuring) Parent?.InvalidateMeasure(); } } protected virtual Vector2 MeasureCore(Vector2 availableSize) { if (!Visible) return default; if (_stylingDirty) ForceRunStyleUpdate(); var withoutMargin = _margin.Deflate(availableSize); var constrained = ApplySizeConstraints(this, withoutMargin); Vector2 measured; try { _measuring = true; measured = Vector2.ComponentMax( MeasureOverride(constrained), // For the time being keep the old CalculateMinimumSize around. #pragma warning disable 618 CalculateMinimumSize()); #pragma warning restore 618 } finally { _measuring = false; } if (!float.IsNaN(SetWidth)) { measured.X = SetWidth; } measured.X = Math.Clamp(measured.X, MinWidth, MaxWidth); if (!float.IsNaN(SetHeight)) { measured.Y = SetHeight; } measured.Y = Math.Clamp(measured.Y, MinHeight, MaxHeight); measured = _margin.Inflate(measured); return Vector2.ComponentMin(measured, availableSize); } protected virtual Vector2 MeasureOverride(Vector2 availableSize) { var min = Vector2.Zero; foreach (var child in Children) { child.Measure(availableSize); min = Vector2.ComponentMax(min, child.DesiredSize); } return min; } public void ArrangePixel(UIBox2i finalRect) { var topLeft = finalRect.TopLeft / UIScale; var bottomRight = finalRect.BottomRight / UIScale; Arrange(new UIBox2(topLeft, bottomRight)); } public void Arrange(UIBox2 finalRect) { if (!IsMeasureValid) Measure(PreviousMeasure ?? finalRect.Size); if (!IsArrangeValid || PreviousArrange != finalRect) { IsArrangeValid = true; ArrangeCore(finalRect); PreviousArrange = finalRect; } } protected virtual void ArrangeCore(UIBox2 finalRect) { if (!Visible) return; var withoutMargins = _margin.Deflate(finalRect); var availWithoutMargins = withoutMargins.Size; var size = availWithoutMargins; var origin = withoutMargins.TopLeft; if (_horizontalAlignment != HAlignment.Stretch) size.X = Math.Min(size.X, DesiredSize.X - _margin.SumHorizontal); if (_verticalAlignment != VAlignment.Stretch) size.Y = Math.Min(size.Y, DesiredSize.Y - _margin.SumVertical); size = ApplySizeConstraints(this, size); Size = size; _isLayoutUpdateOverrideUsed = true; #pragma warning disable 618 LayoutUpdateOverride(); #pragma warning restore 618 if (!_isLayoutUpdateOverrideUsed) { var arranged = ArrangeOverride(size); size = Vector2.ComponentMin(arranged, size); } switch (HorizontalAlignment) { case HAlignment.Stretch: case HAlignment.Center: origin.X += (availWithoutMargins.X - size.X) / 2; break; case HAlignment.Right: origin.X += availWithoutMargins.X - size.X; break; } switch (VerticalAlignment) { case VAlignment.Stretch: case VAlignment.Center: origin.Y += (availWithoutMargins.Y - size.Y) / 2; break; case VAlignment.Bottom: origin.Y += availWithoutMargins.Y - size.Y; break; } Position = origin; Size = size; } protected virtual Vector2 ArrangeOverride(Vector2 finalSize) { foreach (var child in Children) { child.Arrange(UIBox2.FromDimensions(Vector2.Zero, finalSize)); } return finalSize; } [Obsolete("Use Control.ArrangePixel")] protected void FitChildInPixelBox(Control child, UIBox2i pixelBox) { child.ArrangePixel(pixelBox); } [Obsolete("Use Control.Arrange")] protected void FitChildInBox(Control child, UIBox2 box) { child.Arrange(box); } [Obsolete("Implement ArrangeOverride instead.")] protected virtual void LayoutUpdateOverride() { _isLayoutUpdateOverrideUsed = false; } private static Vector2 ApplySizeConstraints(Control control, Vector2 avail) { var minW = control._minWidth; var setW = control._setWidth; var maxW = control._maxWidth; var maxConstraint = float.IsNaN(setW) ? float.PositiveInfinity : setW; maxW = MathHelper.Clamp(maxConstraint, minW, maxW); var minConstraint = float.IsNaN(setW) ? 0 : setW; minW = MathHelper.Clamp(maxW, minConstraint, minW); var minH = control._minHeight; var setH = control._setHeight; var maxH = control._maxHeight; maxConstraint = float.IsNaN(setH) ? float.PositiveInfinity : setH; maxH = MathHelper.Clamp(maxConstraint, minH, maxH); minConstraint = float.IsNaN(setH) ? 0 : setH; minH = MathHelper.Clamp(minW, minConstraint, minH); return ( Math.Clamp(avail.X, minW, maxW), Math.Clamp(avail.Y, minH, maxH)); } /// /// 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, } public enum HAlignment { Stretch, Left, Center, Right } public enum VAlignment { Stretch, Top, Center, Bottom } } }