using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Barotrauma.Extensions; using System.Xml.Linq; namespace Barotrauma { public enum Anchor { TopLeft, TopCenter, TopRight, CenterLeft, Center, CenterRight, BottomLeft, BottomCenter, BottomRight } public enum Pivot { TopLeft, TopCenter, TopRight, CenterLeft, Center, CenterRight, BottomLeft, BottomCenter, BottomRight } public enum ScaleBasis { Normal, BothWidth, BothHeight, Smallest, Largest } public class RectTransform { #region Fields and Properties /// /// Should be assigned only by GUIComponent. /// Note that RectTransform is created first and the GUIComponent after that. /// This means the GUIComponent is not set before the GUIComponent is initialized. /// public GUIComponent GUIComponent { get; set; } private RectTransform parent; public RectTransform Parent { get { return parent; } set { if (parent == value || value == this) { return; } // Remove the child from the old parent RemoveFromHierarchy(displayErrors: false); parent = value; if (parent != null && !parent.children.Contains(this)) { parent.children.Add(this); RecalculateAll(false, true, true); Parent.ChildrenChanged?.Invoke(this); } ParentChanged?.Invoke(parent); } } protected readonly List children = new List(); public IEnumerable Children => children; public int CountChildren => children.Count; private Vector2 relativeSize = Vector2.One; /// /// Relative to the parent rect. /// public Vector2 RelativeSize { get { return relativeSize; } set { if (relativeSize.NearlyEquals(value)) { return; } relativeSize = value; RecalculateAll(resize: true, scale: false, withChildren: true); } } private Point? minSize; /// /// Min size in pixels. /// Does not affect scaling. /// public Point MinSize { get { return minSize ?? Point.Zero; } set { if (minSize == value) { return; } minSize = value; RecalculateAll(true, false, true); } } public readonly static Point MaxPoint = new Point(int.MaxValue, int.MaxValue); private Point? maxSize; /// /// Max size in pixels. /// Does not affect scaling. /// public Point MaxSize { get { return maxSize ?? MaxPoint; } set { if (maxSize == value) { return; } maxSize = value; RecalculateAll(true, false, true); } } private Point nonScaledSize; /// /// Size before scale multiplications. /// public Point NonScaledSize { get { return nonScaledSize; } set { if (nonScaledSize == value) { return; } nonScaledSize = value.Clamp(MinSize, MaxSize); RecalculateRelativeSize(); RecalculateAnchorPoint(); RecalculatePivotOffset(); RecalculateChildren(resize: true, scale: false); } } /// /// Size after scale multiplications. /// public Point ScaledSize => NonScaledSize.Multiply(Scale); /// /// Applied to all RectTransforms. /// The elements are not automatically resized, if the global scale changes. /// You have to manually call RecalculateScale() for all elements after changing the global scale. /// This is because there is currently no easy way to inform all the elements without having a reference to them. /// Having a reference (static list, or event) is problematic, because deconstructing the elements is not handled manually. /// This means that the uncleared references would bloat the memory. /// We could recalculate the scale each time it's needed, /// but in that case the calculation would need to be very lightweight and garbage free, which it currently is not. /// public static Vector2 globalScale = Vector2.One; private Vector2 localScale = Vector2.One; public Vector2 LocalScale { get { return localScale; } set { if (localScale.NearlyEquals(value)) { return; } localScale = value; RecalculateAll(resize: false, scale: true, withChildren: true); ScaleChanged?.Invoke(); } } public Vector2 Scale { get; private set; } private Vector2 relativeOffset = Vector2.Zero; private Point absoluteOffset = Point.Zero; private Point screenSpaceOffset = Point.Zero; /// /// Defined as portions of the parent size. /// Also the direction of the offset is relative, calculated away from the anchor point. /// public Vector2 RelativeOffset { get { return relativeOffset; } set { if (relativeOffset.NearlyEquals(value)) { return; } relativeOffset = value; recalculateRect = true; RecalculateChildren(false, false); } } /// /// Absolute in pixels but relative to the anchor point. /// Calculated away from the anchor point, like a padding. /// Use RelativeOffset to set an amount relative to the parent size. /// public Point AbsoluteOffset { get { return absoluteOffset; } set { if (absoluteOffset == value) { return; } absoluteOffset = value; recalculateRect = true; RecalculateChildren(false, false); } } /// /// Screen space offset. From top left corner. In pixels. /// public Point ScreenSpaceOffset { get { return screenSpaceOffset; } set { if (screenSpaceOffset == value) { return; } screenSpaceOffset = value; recalculateRect = true; RecalculateChildren(false, false); } } /// /// Calculated from the selected pivot. In pixels. /// public Point PivotOffset { get; private set; } /// /// Screen space point in pixels. /// public Point AnchorPoint { get; private set; } public Point TopLeft { get { Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor); Point relativeOffset = ParentRect.MultiplySize(RelativeOffset); relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor); return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset; } } protected Point NonScaledTopLeft { get { Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor); Point relativeOffset = new Point( (int)(NonScaledParentSize.X * RelativeOffset.X), (int)(NonScaledParentSize.Y * RelativeOffset.Y)); relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor); return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset; } } private bool recalculateRect = true; private Rectangle _rect; public Rectangle Rect { get { if (recalculateRect) { _rect = new Rectangle(TopLeft, ScaledSize); recalculateRect = false; } return _rect; } } public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect; protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize); protected virtual Rectangle NonScaledUIRect => NonScaledRect; protected Point NonScaledParentSize => parent?.NonScaledSize ?? new Point(GUI.UIWidth, GameMain.GraphicsHeight); protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : UIRect; protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect; protected Rectangle UIRect => new Rectangle(0, 0, GUI.UIWidth, GameMain.GraphicsHeight); private Pivot pivot; /// /// Does not automatically calculate children. /// Note also that if you change the pivot point with this property, the pivot does not automatically match the anchor. /// You can use SetPosition to change everything automatcally or MatchPivotToAnchor to match the pivot to anchor. /// public Pivot Pivot { get { return pivot; } set { if (pivot == value) { return; } pivot = value; RecalculatePivotOffset(); } } private Anchor anchor; /// /// Does not automatically calculate children. /// Note also that if you change the anchor point with this property, the pivot does not automatically match the anchor. /// You can use SetPosition to change everything automatically or MatchPivotToAnchor to match the pivot to anchor. /// public Anchor Anchor { get { return anchor; } set { if (anchor == value) { return; } anchor = value; RecalculateAnchorPoint(); } } private ScaleBasis _scaleBasis; public ScaleBasis ScaleBasis { get { return _scaleBasis; } set { _scaleBasis = value; RecalculateAbsoluteSize(); RecalculateAnchorPoint(); RecalculatePivotOffset(); } } public bool IsLastChild { get { if (Parent == null) { return false; } var last = Parent.Children.LastOrDefault(); if (last == null) { return false; } return last == this; } } public bool IsFirstChild { get { if (Parent == null) { return false; } var first = Parent.Children.FirstOrDefault(); if (first == null) { return false; } return first == this; } } #endregion #region Events public event Action ParentChanged; /// /// The element provided as the argument is the changed child. It may be new in the hierarchy or just repositioned. /// public event Action ChildrenChanged; public event Action ScaleChanged; public event Action SizeChanged; public void ResetSizeChanged() { SizeChanged = null; } #endregion #region Initialization public RectTransform(Vector2 relativeSize, RectTransform parent, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null, ScaleBasis scaleBasis = ScaleBasis.Normal) { Init(parent, anchor, pivot); _scaleBasis = scaleBasis; this.relativeSize = relativeSize; this.minSize = minSize; this.maxSize = maxSize; RecalculateScale(); RecalculateAbsoluteSize(); RecalculateAnchorPoint(); RecalculatePivotOffset(); parent?.ChildrenChanged?.Invoke(this); } /// /// By default, elements defined with an absolute size (in pixels) will scale with the parent. /// This can be changed by setting IsFixedSize to true. /// public RectTransform(Point absoluteSize, RectTransform parent = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, ScaleBasis scaleBasis = ScaleBasis.Normal, bool isFixedSize = false) { Init(parent, anchor, pivot); _scaleBasis = scaleBasis; this.nonScaledSize = absoluteSize; RecalculateScale(); RecalculateRelativeSize(); if (scaleBasis != ScaleBasis.Normal) { RecalculateAbsoluteSize(); } RecalculateAnchorPoint(); RecalculatePivotOffset(); IsFixedSize = isFixedSize; parent?.ChildrenChanged?.Invoke(this); } public static RectTransform Load(XElement element, RectTransform parent, Anchor defaultAnchor = Anchor.TopLeft) { Enum.TryParse(element.GetAttributeString("anchor", defaultAnchor.ToString()), out Anchor anchor); Enum.TryParse(element.GetAttributeString("pivot", anchor.ToString()), out Pivot pivot); Point? minSize = null, maxSize = null; ScaleBasis scaleBasis = ScaleBasis.Normal; if (element.Attribute("minsize") != null) { minSize = element.GetAttributePoint("minsize", Point.Zero); } if (element.Attribute("maxsize") != null) { maxSize = element.GetAttributePoint("maxsize", new Point(1000, 1000)); } string sb = element.GetAttributeString("scalebasis", null); if (sb != null) { Enum.TryParse(sb, ignoreCase: true, out scaleBasis); } RectTransform rectTransform; if (element.Attribute("absolutesize") != null) { rectTransform = new RectTransform(element.GetAttributePoint("absolutesize", new Point(1000, 1000)), parent, anchor, pivot, scaleBasis) { minSize = minSize, maxSize = maxSize }; } else { rectTransform = new RectTransform(element.GetAttributeVector2("relativesize", Vector2.One), parent, anchor, pivot, minSize, maxSize, scaleBasis); } rectTransform.RelativeOffset = element.GetAttributeVector2("relativeoffset", Vector2.Zero); rectTransform.AbsoluteOffset = element.GetAttributePoint("absoluteoffset", Point.Zero); return rectTransform; } private void Init(RectTransform parent = null, Anchor anchor = Anchor.TopLeft, Pivot? pivot = null) { this.parent = parent; parent?.children.Add(this); Anchor = anchor; Pivot = pivot ?? MatchPivotToAnchor(Anchor); } #endregion #region Protected methods protected void RecalculateScale() { var scale = LocalScale * globalScale; var parents = GetParents(); Scale = parents.Any() ? parents.Select(rt => rt.LocalScale).Aggregate((parent, child) => parent * child) * scale : scale; recalculateRect = true; ScaleChanged?.Invoke(); } protected void RecalculatePivotOffset() { PivotOffset = CalculatePivotOffset(Pivot, ScaledSize); recalculateRect = true; } protected void RecalculateAnchorPoint() { AnchorPoint = CalculateAnchorPoint(Anchor, ParentRect); recalculateRect = true; } protected void RecalculateRelativeSize() { relativeSize = new Vector2(NonScaledSize.X, NonScaledSize.Y) / new Vector2(NonScaledParentUIRect.Width, NonScaledParentUIRect.Height); recalculateRect = true; SizeChanged?.Invoke(); } protected void RecalculateAbsoluteSize() { Point size = NonScaledParentUIRect.Size; switch (ScaleBasis) { case ScaleBasis.BothWidth: size.Y = size.X; break; case ScaleBasis.BothHeight: size.X = size.Y; break; case ScaleBasis.Smallest: if (size.X < size.Y) { size.Y = size.X; } else { size.X = size.Y; } break; case ScaleBasis.Largest: if (size.X > size.Y) { size.Y = size.X; } else { size.X = size.Y; } break; } size = size.Multiply(RelativeSize); nonScaledSize = size.Clamp(MinSize, MaxSize); recalculateRect = true; SizeChanged?.Invoke(); } /// /// If false, the element will resize if the parent is resized (with the children). /// If true, the element will resize only when explicitly resized. /// Note that scaling always affects the elements. /// public bool IsFixedSize { get; set; } protected void RecalculateAll(bool resize, bool scale = true, bool withChildren = true) { if (scale) { RecalculateScale(); } if (resize && !IsFixedSize) { RecalculateAbsoluteSize(); } RecalculateAnchorPoint(); RecalculatePivotOffset(); if (withChildren) { RecalculateChildren(resize, scale); } } private bool RemoveFromHierarchy(bool displayErrors = true) { if (Parent == null) { if (displayErrors) { DebugConsole.ThrowError("Parent null" + Environment.StackTrace.CleanupStackTrace()); } return false; } if (!Parent.children.Contains(this)) { if (displayErrors) { DebugConsole.ThrowError("The children of the parent does not contain this child. This should not be possible! " + Environment.StackTrace.CleanupStackTrace()); } return false; } if (!Parent.children.Remove(this)) { if (displayErrors) { DebugConsole.ThrowError("Unable to remove the child from the parent. " + Environment.StackTrace.CleanupStackTrace()); } return false; } return true; } #endregion #region Public instance methods public void SetPosition(Anchor anchor, Pivot? pivot = null) { Anchor = anchor; Pivot = pivot ?? MatchPivotToAnchor(anchor); ScreenSpaceOffset = Point.Zero; recalculateRect = true; RecalculateChildren(false, false); } public void Resize(Point absoluteSize, bool resizeChildren = true) { nonScaledSize = absoluteSize.Clamp(MinSize, MaxSize); RecalculateRelativeSize(); RecalculateAll(resize: false, scale: false, withChildren: false); RecalculateChildren(resizeChildren, false); } public void Resize(Vector2 relativeSize, bool resizeChildren = true) { this.relativeSize = relativeSize; RecalculateAll(resize: true, scale: false, withChildren: false); RecalculateChildren(resizeChildren, false); } public void ChangeScale(Vector2 newScale) { LocalScale = newScale; } public void ResetScale() { ChangeScale(Vector2.One); } /// /// Currently this needs to be manually called only when the global scale changes. /// If the local scale changes, the scale is automatically recalculated. /// public void RecalculateScale(bool withChildren) { RecalculateScale(); if (withChildren) { RecalculateChildren(resize: false, scale: true); } } /// /// Manipulates ScreenSpaceOffset. /// If you want to manipulate some other offset, access the property setters directly. /// public void Translate(Point translation) { ScreenSpaceOffset += translation; } /// /// Returns all parent elements in the hierarchy. /// public IEnumerable GetParents() { var parents = new List(); if (Parent != null) { parents.Add(Parent); return parents.Concat(Parent.GetParents()); } else { return parents; } } /// /// Returns all child elements in the hierarchy. /// public IEnumerable GetAllChildren() { return children.Concat(children.SelectManyRecursive(c => c.children)); } public int GetChildIndex(RectTransform rectT) { return children.IndexOf(rectT); } public RectTransform GetChild(int index) { return children[index]; } public bool IsParentOf(RectTransform rectT, bool recursive = true) { if (children.Contains(rectT)) { return true; } if (recursive) { foreach (var child in children) { if (child.IsParentOf(rectT)) { return true; } } } return false; } public bool IsChildOf(RectTransform rectT, bool recursive = true) { if (Parent == null) { return false; } return Parent == rectT || (recursive && Parent.IsChildOf(rectT)); } public void ClearChildren() { children.ForEachMod(c => c.Parent = null); } public void SortChildren(Comparison comparison) { children.Sort(comparison); RecalculateAll(false, false, true); Parent.ChildrenChanged?.Invoke(this); } public void ReverseChildren() { children.Reverse(); RecalculateAll(false, false, true); Parent.ChildrenChanged?.Invoke(this); } public void SetAsLastChild() { if (IsLastChild) { return; } if (!RemoveFromHierarchy(displayErrors: true)) { return; } parent.children.Add(this); RecalculateAll(false, true, true); parent.ChildrenChanged?.Invoke(this); } public void SetAsFirstChild() { if (IsFirstChild) { return; } RepositionChildInHierarchy(0); } public bool RepositionChildInHierarchy(int index) { if (!RemoveFromHierarchy(displayErrors: true)) { return false; } try { Parent.children.Insert(index, this); } catch (ArgumentOutOfRangeException e) { DebugConsole.ThrowError(e.ToString()); return false; } RecalculateAll(false, true, true); Parent.ChildrenChanged?.Invoke(this); return true; } public void RecalculateChildren(bool resize, bool scale = true) { for (int i = 0; i < children.Count; i++) { children[i].RecalculateAll(resize, scale, withChildren: true); } } public void AddChildrenToGUIUpdateList(bool ignoreChildren = false, int order = 0) { for (int i = 0; i < children.Count; i++) { children[i].GUIComponent.AddToGUIUpdateList(ignoreChildren, order); } } public void MatchPivotToAnchor() => MatchPivotToAnchor(Anchor); private Point? animTargetPos; public Point AnimTargetPos { get { return animTargetPos ?? AbsoluteOffset; } } public void MoveOverTime(Point targetPos, float duration, Action onDoneMoving = null) { animTargetPos = targetPos; CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration, onDoneMoving)); } public void ScaleOverTime(Point targetSize, float duration) { CoroutineManager.StartCoroutine(DoScaleAnimation(targetSize, duration)); } private IEnumerable DoMoveAnimation(Point targetPos, float duration, Action onDoneMoving = null) { Vector2 startPos = AbsoluteOffset.ToVector2(); float t = 0.0f; while (t < duration && duration > 0.0f) { t += CoroutineManager.DeltaTime; AbsoluteOffset = Vector2.SmoothStep(startPos, targetPos.ToVector2(), t / duration).ToPoint(); yield return CoroutineStatus.Running; } AbsoluteOffset = targetPos; animTargetPos = null; onDoneMoving?.Invoke(); yield return CoroutineStatus.Success; } private IEnumerable DoScaleAnimation(Point targetSize, float duration) { Vector2 startSize = NonScaledSize.ToVector2(); float t = 0.0f; while (t < duration && duration > 0.0f) { t += CoroutineManager.DeltaTime; NonScaledSize = Vector2.SmoothStep(startSize, targetSize.ToVector2(), t / duration).ToPoint(); yield return CoroutineStatus.Running; } NonScaledSize = targetSize; yield return CoroutineStatus.Success; } #endregion #region Static methods public static Pivot MatchPivotToAnchor(Anchor anchor) { if (!Enum.TryParse(anchor.ToString(), out Pivot pivot)) { throw new Exception($"[RectTransform] Cannot match pivot to anchor {anchor}"); } return pivot; } /// /// Converts the offset so that the direction is always away from the anchor point. /// public static Point ConvertOffsetRelativeToAnchor(Point offset, Anchor anchor) { switch (anchor) { case Anchor.BottomRight: return offset.Inverse(); case Anchor.BottomLeft: case Anchor.BottomCenter: return new Point(offset.X, -offset.Y); case Anchor.TopRight: case Anchor.CenterRight: return new Point(-offset.X, offset.Y); default: return offset; } } public static Point CalculatePivotOffset(Pivot pivot, Point size) { int width = size.X; int height = size.Y; switch (pivot) { case Pivot.TopLeft: return Point.Zero; case Pivot.TopCenter: return new Point(-width / 2, 0); case Pivot.TopRight: return new Point(-width, 0); case Pivot.CenterLeft: return new Point(0, -height / 2); case Pivot.Center: return size.Divide(2).Inverse(); case Pivot.CenterRight: return new Point(-width, -height / 2); case Pivot.BottomLeft: return new Point(0, -height); case Pivot.BottomCenter: return new Point(-width / 2, -height); case Pivot.BottomRight: return new Point(-width, -height); default: throw new NotImplementedException(pivot.ToString()); } } public static Point CalculateAnchorPoint(Anchor anchor, Rectangle parent) { switch (anchor) { case Anchor.TopLeft: return parent.Location; case Anchor.TopCenter: return new Point(parent.Center.X, parent.Top); case Anchor.TopRight: return new Point(parent.Right, parent.Top); case Anchor.CenterLeft: return new Point(parent.Left, parent.Center.Y); case Anchor.Center: return parent.Center; case Anchor.CenterRight: return new Point(parent.Right, parent.Center.Y); case Anchor.BottomLeft: return new Point(parent.Left, parent.Bottom); case Anchor.BottomCenter: return new Point(parent.Center.X, parent.Bottom); case Anchor.BottomRight: return new Point(parent.Right, parent.Bottom); default: throw new NotImplementedException(anchor.ToString()); } } /// /// The elements are not automatically resized, if the global scale changes. /// You have to manually call RecalculateScale() for all elements after changing the global scale. /// This is because there is currently no easy way to inform all the elements without having a reference to them. /// Having a reference (static list, or event) is problematic, because deconstructing the elements is not handled manually. /// This means that the uncleared references would bloat the memory. /// We could recalculate the scale each time it's needed, /// but in that case the calculation would need to be very lightweight and garbage free, which it currently is not. /// public static void ResetGlobalScale() { globalScale = Vector2.One; } #endregion } }