Files
RobustToolbox/Robust.Client/UserInterface/Control.Layout.cs
T

816 lines
25 KiB
C#

using System;
using JetBrains.Annotations;
using Robust.Client.Graphics;
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 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();
}
}
/// <summary>
/// Called when the <see cref="UIScale"/> for this control changes.
/// </summary>
protected internal virtual void UIScaleChanged()
{
InvalidateMeasure();
}
/// <summary>
/// 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
/// </summary>
[ViewVariables]
public virtual float UIScale => Root?.UIScale ?? 1;
/// <summary>
/// The size of this control, in virtual pixels.
/// </summary>
/// <seealso cref="PixelSize"/>
/// <seealso cref="Width"/>
/// <seealso cref="Height"/>
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Size
{
get => _size;
internal set
{
if (_size == value)
{
return;
}
_size = value;
Resized();
}
}
/// <summary>
/// The size of this control, in physical pixels.
/// </summary>
[ViewVariables]
public Vector2i PixelSize => (Vector2i) (_size * UIScale);
/// <summary>
/// A <see cref="UIBox2"/> with the top left at 0,0 and the size equal to <see cref="Size"/>.
/// </summary>
/// <seealso cref="PixelSizeBox"/>
public UIBox2 SizeBox => new(Vector2.Zero, Size);
/// <summary>
/// A <see cref="UIBox2i"/> with the top left at 0,0 and the size equal to <see cref="PixelSize"/>.
/// </summary>
/// <seealso cref="SizeBox"/>
public UIBox2i PixelSizeBox => new(Vector2i.Zero, PixelSize);
/// <summary>
/// The width of the control, in virtual pixels.
/// </summary>
/// <seealso cref="PixelWidth"/>
public float Width => Size.X;
/// <summary>
/// The height of the control, in virtual pixels.
/// </summary>
/// <seealso cref="PixelHeight"/>
public float Height => Size.Y;
/// <summary>
/// The width of the control, in physical pixels.
/// </summary>
/// <seealso cref="Width"/>
public int PixelWidth => PixelSize.X;
/// <summary>
/// The height of the control, in physical pixels.
/// </summary>
/// <seealso cref="Height"/>
public int PixelHeight => PixelSize.Y;
/// <summary>
/// The position of the top left corner of the control, in virtual pixels.
/// This is relative to the position of the parent.
/// </summary>
/// <seealso cref="PixelPosition"/>
/// <seealso cref="GlobalPosition"/>
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 Position { get; internal set; }
/// <summary>
/// The position of the top left corner of the control, in physical pixels.
/// </summary>
/// <seealso cref="Position"/>
[ViewVariables]
public Vector2i PixelPosition => (Vector2i) (Position * UIScale);
/// <summary>
/// The position of the top left corner of the control, in virtual pixels.
/// This is not relative to the parent.
/// </summary>
/// <seealso cref="GlobalPosition"/>
/// <seealso cref="Position"/>
[ViewVariables]
public Vector2 GlobalPosition
{
get
{
var offset = Position;
var parent = Parent;
while (parent != null)
{
offset += parent.Position;
parent = parent.Parent;
}
return offset;
}
}
/// <summary>
/// The position of the top left corner of the control, in physical pixels.
/// This is not relative to the parent.
/// </summary>
/// <seealso cref="GlobalPosition"/>
[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;
}
}
/// <summary>
/// Represents the "rectangle" of the control relative to the parent, in virtual pixels.
/// </summary>
/// <seealso cref="PixelRect"/>
public UIBox2 Rect => UIBox2.FromDimensions(Position, _size);
/// <summary>
/// Represents the "rectangle" of the control relative to the parent, in physical pixels.
/// </summary>
/// <seealso cref="Rect"/>
public UIBox2i PixelRect => UIBox2i.FromDimensions(PixelPosition, PixelSize);
/// <summary>
/// Horizontal size flags for container layout.
/// </summary>
[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();
}
}
/// <summary>
/// Vertical size flags for container layout.
/// </summary>
[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();
}
}
/// <summary>
/// Stretch ratio used to give shared of the available space in case multiple siblings are set to expand
/// in a container
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value is less than or equal to 0.
/// </exception>
[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();
}
}
/// <summary>
/// A combination of <see cref="MinSize" /> and <see cref="CalculateMinimumSize" />,
/// Whichever is greater.
/// Use this for whenever you need the *actual* minimum size of something.
/// </summary>
/// <remarks>
/// This is in virtual pixels.
/// </remarks>
/// <seealso cref="CombinedPixelMinimumSize"/>
[Obsolete("Use DesiredSize and Measure()")]
public Vector2 CombinedMinimumSize => DesiredSize;
/// <summary>
/// The <see cref="CombinedMinimumSize"/>, in physical pixels.
/// </summary>
[Obsolete("Use DesiredSize and Measure()")]
public Vector2i CombinedPixelMinimumSize => (Vector2i) (CombinedMinimumSize * UIScale);
/// <summary>
/// A custom minimum size. If the control-calculated size is is smaller than this, this is used instead.
/// </summary>
/// <seealso cref="CalculateMinimumSize" />
/// <seealso cref="CombinedMinimumSize" />
[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();
}
}
/// <summary>
/// Override this to calculate a minimum size for this control.
/// Do NOT call this directly to get the minimum size for layout purposes!
/// Use <see cref="CombinedMinimumSize" /> for the ACTUAL minimum size.
/// </summary>
[Obsolete("Implement MeasureOverride instead")]
protected virtual Vector2 CalculateMinimumSize()
{
return Vector2.Zero;
}
/// <summary>
/// Tells the GUI system that the minimum size of this control may have changed,
/// so that say containers will re-sort it if necessary.
/// </summary>
[Obsolete("Use InvalidateMeasure()")]
public void MinimumSizeChanged()
{
InvalidateMeasure();
}
public void InvalidateMeasure()
{
if (!IsMeasureValid)
return;
IsMeasureValid = false;
IsArrangeValid = false;
UserInterfaceManagerInternal.QueueMeasureUpdate(this);
}
/// <summary>
/// Forces this component to immediately calculate layout.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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));
}
/// <summary>
/// Controls how a control changes size when inside a container.
/// </summary>
[Flags]
[PublicAPI]
public enum SizeFlags : byte
{
/// <summary>
/// Shrink to the begin of the specified axis.
/// </summary>
None = 0,
/// <summary>
/// Fill as much space as possible in a container, without pushing others.
/// </summary>
Fill = 1,
/// <summary>
/// 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 <see cref="SizeFlagsStretchRatio" />
/// </summary>
Expand = 2,
/// <summary>
/// Combination of <see cref="Fill" /> and <see cref="Expand" />.
/// </summary>
FillExpand = 3,
/// <summary>
/// Shrink inside a container, aligning to the center.
/// </summary>
ShrinkCenter = 4,
/// <summary>
/// Shrink inside a container, aligning to the end.
/// </summary>
ShrinkEnd = 8,
}
public enum HAlignment
{
Stretch,
Left,
Center,
Right
}
public enum VAlignment
{
Stretch,
Top,
Center,
Bottom
}
}
}