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;