using System; using System.Numerics; using Robust.Client.Graphics; using Robust.Client.UserInterface.Controls; using Robust.Shared.Animations; using Robust.Shared.Map; 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 const float DefaultStretchRatio = 1; private const float DefaultSetSize = float.NaN; private const float DefaultMaxSize = float.PositiveInfinity; private const HAlignment DefaultHAlignment = HAlignment.Stretch; private const VAlignment DefaultVAlignment = VAlignment.Stretch; private Vector2 _size; [ViewVariables] internal Vector2? PreviousMeasure; [ViewVariables] internal UIBox2? PreviousArrange; private float _sizeFlagsStretchRatio = DefaultStretchRatio; private float _minWidth; private float _minHeight; private float _setWidth = DefaultSetSize; private float _setHeight = DefaultSetSize; private float _maxWidth = DefaultMaxSize; private float _maxHeight = DefaultMaxSize; private bool _horizontalExpand; private bool _verticalExpand; private HAlignment _horizontalAlignment = DefaultHAlignment; private VAlignment _verticalAlignment = DefaultVAlignment; private Thickness _margin; private bool _measuring; private bool _arranging; /// /// The desired minimum size this control needs for layout to avoid cutting off content or such. /// /// /// This is calculated by calling . /// [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; } /// /// Controls the amount of empty space in virtual pixels around the control. /// /// Values can be provided as "All" or "Horizontal, Vertical" or "Left, Top, Right, Bottom" [ViewVariables] public Thickness Margin { get => _margin; set { _margin = value; SetLayoutStyleProp(LayoutStyleProperties.Margin); 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] public virtual float UIScale => Root?.UIScale ?? 1; /// /// 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 * 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 * 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; } } [ViewVariables] public virtual IClydeWindow? Window => Root?.Window; [ViewVariables] public virtual ScreenCoordinates ScreenCoordinates { get { // TODO: optimize for single tree walk. var window = Window; return window != null ? new(GlobalPixelPosition, window.Id) : default; } } /// /// 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); public UIBox2 GlobalRect => UIBox2.FromDimensions(GlobalPosition, _size); public UIBox2i GlobalPixelRect => UIBox2i.FromDimensions(GlobalPixelPosition, PixelSize); /// /// Horizontal alignment mode. /// This determines how the control should be laid out horizontally /// if it gets more available space than its . /// [ViewVariables(VVAccess.ReadWrite)] public HAlignment HorizontalAlignment { get => _horizontalAlignment; set { _horizontalAlignment = value; SetLayoutStyleProp(LayoutStyleProperties.HorizontalAlignment); InvalidateArrange(); } } /// /// Vertical alignment mode. /// This determines how the control should be laid out vertically /// if it gets more available space than its . /// [ViewVariables(VVAccess.ReadWrite)] public VAlignment VerticalAlignment { get => _verticalAlignment; set { _verticalAlignment = value; SetLayoutStyleProp(LayoutStyleProperties.VerticalAlignment); InvalidateArrange(); } } /// /// Whether to horizontally expand and push other controls in layout controls that support this. /// This does nothing unless the parent is a control like which supports this behavior. /// /// /// If I was redesigning the UI system from scratch today, this would be an attached property instead. /// [ViewVariables(VVAccess.ReadWrite)] public bool HorizontalExpand { get => _horizontalExpand; set { _horizontalExpand = value; SetLayoutStyleProp(LayoutStyleProperties.HorizontalExpand); Parent?.InvalidateMeasure(); } } /// /// Whether to vertically expand and push other controls in layout controls that support this. /// This does nothing unless the parent is a control like which supports this behavior. /// /// /// If I was redesigning the UI system from scratch today, this would be an attached property instead. /// [ViewVariables(VVAccess.ReadWrite)] public bool VerticalExpand { get => _verticalExpand; set { _verticalExpand = value; SetLayoutStyleProp(LayoutStyleProperties.VerticalExpand); 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; SetLayoutStyleProp(LayoutStyleProperties.StretchRatio); Parent?.InvalidateArrange(); } } /// /// A settable minimum size for this control. /// This is factored into so that this control itself always has at least this size. /// /// /// /// This is not to be confused with , /// which contains the actual calculated minimum size from the layout system. /// This property is just an input parameter. /// /// /// If , and/or are in conflict, /// is the most important, then , then . /// /// /// /// [Animatable] public Vector2 MinSize { get => new(_minWidth, _minHeight); set => (MinWidth, MinHeight) = Vector2.Max(Vector2.Zero, value); } /// /// A settable exact size for this control. /// This is factored into so that this control itself always has exactly this size. /// /// /// /// This is not to be confused with , /// which contains the actual calculated size from the layout system. /// This property is just an input parameter. /// /// /// If , and/or are in conflict, /// is the most important, then , then . /// /// /// /// [Animatable] public Vector2 SetSize { get => new(_setWidth, _setHeight); set => (SetWidth, SetHeight) = value; } /// /// A settable maximum size for this control. /// This is factored into so that this control itself always has at most this size. /// /// /// If , and/or are in conflict, /// is the most important, then , then . /// /// /// public Vector2 MaxSize { get => new(_maxWidth, _maxHeight); set => (MaxWidth, MaxHeight) = value; } /// /// Width component of . /// [ViewVariables(VVAccess.ReadWrite)] public float MinWidth { get => _minWidth; set { _minWidth = value; SetLayoutStyleProp(LayoutStyleProperties.MinWidth); InvalidateMeasure(); } } /// /// Height component of . /// [ViewVariables(VVAccess.ReadWrite)] public float MinHeight { get => _minHeight; set { _minHeight = value; SetLayoutStyleProp(LayoutStyleProperties.MinHeight); InvalidateMeasure(); } } /// /// Width component of . /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public float SetWidth { get => _setWidth; set { _setWidth = value; SetLayoutStyleProp(LayoutStyleProperties.SetWidth); InvalidateMeasure(); } } /// /// Height component of . /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public float SetHeight { get => _setHeight; set { _setHeight = value; SetLayoutStyleProp(LayoutStyleProperties.SetHeight); InvalidateMeasure(); } } /// /// Width component of . /// [ViewVariables(VVAccess.ReadWrite)] public float MaxWidth { get => _maxWidth; set { _maxWidth = value; SetLayoutStyleProp(LayoutStyleProperties.MaxWidth); InvalidateMeasure(); } } /// /// Height component of . /// [ViewVariables(VVAccess.ReadWrite)] public float MaxHeight { get => _maxHeight; set { _maxHeight = value; SetLayoutStyleProp(LayoutStyleProperties.MaxHeight); InvalidateMeasure(); } } /// /// Gets the screen coordinates position relative to the control. /// public Vector2 GetLocalPosition(ScreenCoordinates coordinates) { return coordinates.Position - GlobalPixelPosition; } /// /// Notify the layout system that this control's result may have changed /// and must be recalculated. /// public void InvalidateMeasure() { if (!IsMeasureValid || _measuring) return; IsMeasureValid = false; UserInterfaceManagerInternal.QueueMeasureUpdate(this); InvalidateArrange(); } /// /// Notify the layout system that this control's result may have changed /// and must be recalculated. /// public void InvalidateArrange() { if (!IsArrangeValid || _arranging) { // Already queued for a layout update, don't bother. return; } IsArrangeValid = false; UserInterfaceManagerInternal.QueueArrangeUpdate(this); } /// /// Measure the desired size of this control, if given a specific available space. /// The result of this measure is stored in . /// /// /// Available size is given to this method so that controls can handle special cases such as text layout, /// where word wrapping can cause the vertical size to change based on available horizontal size. /// /// The space available to this control, that it should measure for. public void Measure(Vector2 availableSize) { if (!IsMeasureValid || PreviousMeasure != availableSize) { IsMeasureValid = true; _measuring = true; Vector2 desired; try { desired = MeasureCore(availableSize); } finally { _measuring = false; } 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(); } } /// /// Core logic implementation of , /// implementing stuff such as margins and . /// In almost all cases, you want to override instead, which is called by this. /// /// The actual measured desired size of the control. protected virtual Vector2 MeasureCore(Vector2 availableSize) { if (!(Visible || ReservesSpace)) return default; if (_stylingDirty) ForceRunStyleUpdate(); var withoutMargin = _margin.Deflate(availableSize); var constrained = ApplySizeConstraints(this, withoutMargin); var measured = MeasureOverride(constrained); 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); measured = Vector2.Min(measured, availableSize); measured = Vector2.Max(measured, Vector2.Zero); return measured; } /// /// Calculates the actual desired size for the contents of this control, based on available size. /// protected virtual Vector2 MeasureOverride(Vector2 availableSize) { var min = Vector2.Zero; foreach (var child in Children) { child.Measure(availableSize); min = Vector2.Max(min, child.DesiredSize); } return min; } /// /// Lay out this control in the given space of its parent, by pixel coordinates. /// public void ArrangePixel(UIBox2i finalRect) { var topLeft = finalRect.TopLeft / UIScale; var bottomRight = finalRect.BottomRight / UIScale; Arrange(new UIBox2(topLeft, bottomRight)); } /// /// Lay out this control in the given space of its parent. /// This sets and and also arranges any child controls. /// public void Arrange(UIBox2 finalRect) { _arranging = true; try { if (!IsMeasureValid) Measure(PreviousMeasure ?? finalRect.Size); if (!IsArrangeValid || PreviousArrange != finalRect) { IsArrangeValid = true; ArrangeCore(finalRect); PreviousArrange = finalRect; } } finally { _arranging = false; } } /// /// Core logic implementation of , /// implementing stuff such as margins and . /// In almost all cases, you want to override instead, which is called by this. /// protected virtual void ArrangeCore(UIBox2 finalRect) { if (!(Visible || ReservesSpace)) 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); var arranged = ArrangeOverride(size); size = Vector2.Min(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; } /// /// Lay out this control and its children for the specified final size. /// /// /// The final size for this control, /// after calculation of things like margins and alignment. /// /// The actual space used by this control. protected virtual Vector2 ArrangeOverride(Vector2 finalSize) { foreach (var child in Children) { child.Arrange(UIBox2.FromDimensions(Vector2.Zero, finalSize)); } return finalSize; } 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(minConstraint, minH, maxH); return new Vector2( Math.Clamp(avail.X, minW, maxW), Math.Clamp(avail.Y, minH, maxH)); } /// /// Specifies horizontal alignment modes. /// /// public enum HAlignment { /// /// The control should take up all available horizontal space. /// Stretch, /// /// The control should take up minimal () space and align to the left of its given space. /// Left, /// /// The control should take up minimal () space and align in the center of its given space. /// Center, /// /// The control should take up minimal () space and align to the right of its given space. /// Right } /// /// Specifies vertical alignment modes. /// /// public enum VAlignment { /// /// The control should take up all available vertical space. /// Stretch, /// /// The control should take up minimal () space and align to the top of its given space. /// Top, /// /// The control should take up minimal () space and align in the center of its given space. /// Center, /// /// The control should take up minimal () space and align to the bottom of its given space. /// Bottom } } }