diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c6a5fa90c..282d6d159 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Add validation to `UiBox2` to ensure the size is >= 0. ### New features diff --git a/Robust.Client.Tests/Graphics/StyleBoxTest.cs b/Robust.Client.Tests/Graphics/StyleBoxTest.cs index ec1a28918..3e732e94d 100644 --- a/Robust.Client.Tests/Graphics/StyleBoxTest.cs +++ b/Robust.Client.Tests/Graphics/StyleBoxTest.cs @@ -36,5 +36,21 @@ public void TestGetEnvelopBox() styleBox.GetEnvelopBox(new Vector2(10, 10), new Vector2(50, 50), 2.0f), Is.EqualTo(new UIBox2(10, 10, 80, 92))); } + + [Test] + public void TestGetContentBoxClampsWhenMarginsExceedBaseBox() + { + var styleBox = new StyleBoxFlat + { + ContentMarginLeftOverride = 10, + ContentMarginTopOverride = 20, + ContentMarginRightOverride = 30, + ContentMarginBottomOverride = 40, + }; + + var contentBox = styleBox.GetContentBox(new UIBox2(0, 0, 5, 5), 1); + + Assert.That(contentBox, Is.EqualTo(new UIBox2(10, 20, 10, 20))); + } } } diff --git a/Robust.Client/Graphics/Drawing/StyleBox.cs b/Robust.Client/Graphics/Drawing/StyleBox.cs index 05e14474a..29075d65e 100644 --- a/Robust.Client/Graphics/Drawing/StyleBox.cs +++ b/Robust.Client/Graphics/Drawing/StyleBox.cs @@ -276,9 +276,6 @@ public Vector2 GetContentOffset(Vector2 basePosition, float uiScale) /// Gets the box considered the "contents" of this style box, when drawn at a specific size. Input and output /// boxes are in virtual pixels, though virtual pixels can also be used if the ui scale is set to 1. /// - /// - /// is too small and the resultant box would have negative dimensions. - /// public UIBox2 GetContentBox(UIBox2 baseBox, float uiScale) { var left = baseBox.Left + GetContentMargin(Margin.Left) * uiScale; @@ -286,6 +283,9 @@ public UIBox2 GetContentBox(UIBox2 baseBox, float uiScale) var right = baseBox.Right - GetContentMargin(Margin.Right) * uiScale; var bottom = baseBox.Bottom - GetContentMargin(Margin.Bottom) * uiScale; + right = MathF.Max(left, right); + bottom = MathF.Max(top, bottom); + return new UIBox2(left, top, right, bottom); } diff --git a/Robust.Shared.Maths.Tests/UIBox2_Test.cs b/Robust.Shared.Maths.Tests/UIBox2_Test.cs index b3941482d..b7859309b 100644 --- a/Robust.Shared.Maths.Tests/UIBox2_Test.cs +++ b/Robust.Shared.Maths.Tests/UIBox2_Test.cs @@ -97,6 +97,32 @@ public void Box2EdgesConstructor([ValueSource(nameof(Sources))] (float, float, f Assert.That(box.Bottom, Is.EqualTo(bottom)); } + [Test] + public void Box2ValidatesConstruction() + { + Assert.Multiple(() => + { + Assert.Throws(() => new UIBox2(3, 4, -1, -2)); + Assert.Throws(() => new UIBox2(new Vector2(3, 4), new Vector2(-1, -2))); + }); + } + + [Test] + public void Box2ValidatesProperties() + { + var box = new UIBox2(-1, -2, 3, 4); + + Assert.Multiple(() => + { + Assert.Throws(() => box.Left = 4); + Assert.Throws(() => box.Top = 5); + Assert.Throws(() => box.Right = -2); + Assert.Throws(() => box.Bottom = -3); + Assert.Throws(() => box.TopLeft = new Vector2(4, 0)); + Assert.Throws(() => box.BottomRight = new Vector2(0, -3)); + }); + } + [Test] public void Box2CornerVectorProperties([ValueSource(nameof(Sources))] (float, float, float, float) test) { diff --git a/Robust.Shared.Maths/UIBox2.cs b/Robust.Shared.Maths/UIBox2.cs index bc5fd63f4..99a45a252 100644 --- a/Robust.Shared.Maths/UIBox2.cs +++ b/Robust.Shared.Maths/UIBox2.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -17,83 +18,251 @@ public struct UIBox2 : IEquatable, ISpanFormattable /// /// The X coordinate of the left edge of the box. /// - [FieldOffset(sizeof(float) * 0)] public float Left; + [FieldOffset(sizeof(float) * 0)] internal float _left; /// /// The Y coordinate of the top edge of the box. /// - [FieldOffset(sizeof(float) * 1)] public float Top; + [FieldOffset(sizeof(float) * 1)] internal float _top; /// /// The X coordinate of the right edge of the box. /// - [FieldOffset(sizeof(float) * 2)] public float Right; + [FieldOffset(sizeof(float) * 2)] internal float _right; /// /// The Y coordinate of the bottom of the box. /// - [FieldOffset(sizeof(float) * 3)] public float Bottom; + [FieldOffset(sizeof(float) * 3)] internal float _bottom; - [FieldOffset(sizeof(float) * 0)] public Vector2 TopLeft; - [FieldOffset(sizeof(float) * 2)] public Vector2 BottomRight; + [FieldOffset(sizeof(float) * 0)] internal Vector2 _topLeft; + [FieldOffset(sizeof(float) * 2)] internal Vector2 _bottomRight; - public readonly Vector2 TopRight => new(Right, Top); - public readonly Vector2 BottomLeft => new(Left, Bottom); - public readonly float Width => MathF.Abs(Right - Left); - public readonly float Height => MathF.Abs(Top - Bottom); - public readonly Vector2 Size => new(Width, Height); - public readonly Vector2 Center => TopLeft + Size / 2; + /// + /// The X coordinate of the left edge of the box. + /// + public float Left + { + readonly get => _left; + set + { + if (value > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "Left cannot be greater than Right."); + + _left = value; + } + } + + /// + /// The Y coordinate of the top edge of the box. + /// + public float Top + { + readonly get => _top; + set + { + if (value > _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "Top cannot be greater than Bottom."); + + _top = value; + } + } + + /// + /// The X coordinate of the right edge of the box. + /// + public float Right + { + readonly get => _right; + set + { + if (value < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "Right cannot be less than Left."); + + _right = value; + } + } + + /// + /// The Y coordinate of the bottom of the box. + /// + public float Bottom + { + readonly get => _bottom; + set + { + if (value < _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "Bottom cannot be less than Top."); + + _bottom = value; + } + } + + public Vector2 TopLeft + { + readonly get => _topLeft; + set + { + if (value.X > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopLeft.X cannot be greater than Right."); + + if (value.Y > _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopLeft.Y cannot be greater than Bottom."); + + _topLeft = value; + } + } + + public Vector2 BottomRight + { + readonly get => _bottomRight; + set + { + if (value.X < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomRight.X cannot be less than Left."); + + if (value.Y < _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomRight.Y cannot be less than Top."); + + _bottomRight = value; + } + } + + public readonly Vector2 TopRight + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(Right, Top); + } + + public readonly Vector2 BottomLeft + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(Left, Bottom); + } + + public readonly float Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _right - _left; + } + + public readonly float Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _bottom - _top; + } + + public readonly Vector2 Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(Width, Height); + } + + public readonly Vector2 Center + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new Vector2(_left + _right, _top + _bottom) / 2f; + } + + private static void Validate(float left, float top, float right, float bottom) + { + if (left > right) + throw new ArgumentException("Left cannot be greater than Right.", nameof(left)); + + if (top > bottom) + throw new ArgumentException("Top cannot be greater than Bottom.", nameof(top)); + } public UIBox2(Vector2 leftTop, Vector2 rightBottom) { Unsafe.SkipInit(out this); - TopLeft = leftTop; - BottomRight = rightBottom; + Validate(leftTop.X, leftTop.Y, rightBottom.X, rightBottom.Y); + + _topLeft = leftTop; + _bottomRight = rightBottom; } public UIBox2(float left, float top, float right, float bottom) { Unsafe.SkipInit(out this); - Left = left; - Right = right; - Top = top; - Bottom = bottom; + Validate(left, top, right, bottom); + + _left = left; + _right = right; + _top = top; + _bottom = bottom; } + /// + /// Creates a UIBox2 with no bounds validation applied, use at your own risk. + /// + internal static UIBox2 DangerousCreate(float left, float top, float right, float bottom) + { + Unsafe.SkipInit(out UIBox2 box); + box._left = left; + box._right = right; + box._top = top; + box._bottom = bottom; + return box; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public static UIBox2 FromDimensions(float left, float top, float width, float height) { return new(left, top, left + width, top + height); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public static UIBox2 FromDimensions(Vector2 leftTopPosition, Vector2 size) { return FromDimensions(leftTopPosition.X, leftTopPosition.Y, size.X, size.Y); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public readonly bool Intersects(UIBox2 other) { - return other.Bottom >= this.Top && other.Top <= this.Bottom && other.Right >= this.Left && - other.Left <= this.Right; + return other._bottom >= _top + && other._top <= _bottom + && other._right >= _left + && other._left <= _right; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public readonly bool IsEmpty() { return MathHelper.CloseToPercent(Width, 0.0f) && MathHelper.CloseToPercent(Height, 0.0f); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public readonly bool Encloses(UIBox2 inner) { - return this.Left < inner.Left && this.Bottom > inner.Bottom && this.Right > inner.Right && - this.Top < inner.Top; + return Left < inner.Left && Bottom > inner.Bottom && Right > inner.Right && Top < inner.Top; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] + public readonly bool Contains(in UIBox2 inner) + => Left <= inner.Left + && Top <= inner.Top + && Right >= inner.Right + && Bottom >= inner.Bottom; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public readonly bool Contains(float x, float y) { return Contains(new Vector2(x, y)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public readonly bool Contains(Vector2 point, bool closedRegion = true) { var xOk = closedRegion @@ -112,6 +281,8 @@ public readonly bool Contains(Vector2 point, bool closedRegion = true) /// /// Value to scale the box by. /// Scaled box. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public readonly UIBox2 Scale(float scalar) { if (scalar < 0) @@ -127,23 +298,28 @@ public readonly UIBox2 Scale(float scalar) } /// Returns a UIBox2 translated by the given amount. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Pure] public readonly UIBox2 Translated(Vector2 point) { return new(Left + point.X, Top + point.Y, Right + point.X, Bottom + point.Y); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool Equals(UIBox2 other) { return Left.Equals(other.Left) && Right.Equals(other.Right) && Top.Equals(other.Top) && Bottom.Equals(other.Bottom); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override readonly bool Equals(object? obj) { if (obj is null) return false; return obj is UIBox2 box2 && Equals(box2); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public override readonly int GetHashCode() { unchecked @@ -159,6 +335,7 @@ public override readonly int GetHashCode() /// /// Compares two objects for equality by value. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(UIBox2 a, UIBox2 b) { return MathHelper.CloseToPercent(a.Bottom, b.Bottom) && @@ -167,11 +344,13 @@ public override readonly int GetHashCode() MathHelper.CloseToPercent(a.Left, b.Left); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(UIBox2 a, UIBox2 b) { return !(a == b); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static UIBox2 operator +(UIBox2 box, (float lo, float to, float ro, float bo) offsets) { var (lo, to, ro, bo) = offsets;