Files
RobustToolbox/Robust.Client/UserInterface/Controls/LayoutContainer.cs

701 lines
28 KiB
C#

using System;
using System.Numerics;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.ViewVariables;
namespace Robust.Client.UserInterface.Controls
{
[PublicAPI]
[Virtual]
public class LayoutContainer : Container
{
/// <summary>
/// The value of an anchor that is exactly on the begin of the parent control.
/// </summary>
public const float AnchorBegin = 0;
/// <summary>
/// The value of an anchor that is exactly on the end of the parent control.
/// </summary>
public const float AnchorEnd = 1;
[ViewVariables(VVAccess.ReadWrite)] public bool Debug { get; set; }
/// <summary>
/// If true, measurements of this control will be at least the size of any contained controls.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool InheritChildMeasure
{
get => _inheritChildMeasure;
set
{
_inheritChildMeasure = value;
InvalidateMeasure();
}
}
public static readonly AttachedProperty MarginLeftProperty = AttachedProperty.Create("MarginLeft",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty MarginTopProperty = AttachedProperty.Create("MarginTop",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty MarginRightProperty = AttachedProperty.Create("MarginRight",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty MarginBottomProperty = AttachedProperty.Create("MarginBottom",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty AnchorLeftProperty = AttachedProperty.Create("AnchorLeft",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty AnchorTopProperty = AttachedProperty.Create("AnchorTop",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty AnchorRightProperty = AttachedProperty.Create("AnchorRight",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty AnchorBottomProperty = AttachedProperty.Create("AnchorBottom",
typeof(LayoutContainer), typeof(float), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty GrowHorizontalProperty = AttachedProperty.Create("GrowHorizontal",
typeof(LayoutContainer), typeof(GrowDirection), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty GrowVerticalProperty = AttachedProperty.Create("GrowVertical",
typeof(LayoutContainer), typeof(GrowDirection), changed: LayoutPropertyChangedCallback);
public static readonly AttachedProperty<bool> DebugProperty = AttachedProperty<bool>.Create("Debug",
typeof(LayoutContainer));
private bool _inheritChildMeasure = true;
public static void SetMarginLeft(Control control, float value)
{
control.SetValue(MarginLeftProperty, value);
}
public static void SetMarginTop(Control control, float value)
{
control.SetValue(MarginTopProperty, value);
}
public static void SetMarginRight(Control control, float value)
{
control.SetValue(MarginRightProperty, value);
}
public static void SetMarginBottom(Control control, float value)
{
control.SetValue(MarginBottomProperty, value);
}
public static void SetAnchorLeft(Control control, float value)
{
control.SetValue(AnchorLeftProperty, value);
}
public static void SetAnchorTop(Control control, float value)
{
control.SetValue(AnchorTopProperty, value);
}
public static void SetAnchorRight(Control control, float value)
{
control.SetValue(AnchorRightProperty, value);
}
public static void SetAnchorBottom(Control control, float value)
{
control.SetValue(AnchorBottomProperty, value);
}
public static void SetGrowHorizontal(Control control, GrowDirection value)
{
control.SetValue(GrowHorizontalProperty, value);
}
public static void SetGrowVertical(Control control, GrowDirection value)
{
control.SetValue(GrowVerticalProperty, value);
}
public static void SetPosition(Control control, Vector2 position)
{
var (diffX, diffY) = position - control.Position;
// This is just to make subsequent set calls work correctly.
// It should get reset to this exact value next update either way.
control.Position = position;
SetMarginLeft(control, diffX + control.GetValue<float>(MarginLeftProperty));
SetMarginTop(control, diffY + control.GetValue<float>(MarginTopProperty));
SetMarginRight(control, diffX + control.GetValue<float>(MarginRightProperty));
SetMarginBottom(control, diffY + control.GetValue<float>(MarginBottomProperty));
}
/// <summary>
/// Sets an anchor AND a margin preset. This is most likely the method you want.
///
/// </summary>
/// <remarks>
/// Note that the current size and minimum size of the control affects how
/// each of the margins will be set, so if your control needs to shrink beyond its
/// current size / min size, you should either not call this method or only call it when your
/// control has a size of (0, 0). Otherwise your control's size will never be able
/// to go below the size implied by the margins set in this method.
/// </remarks>
public static void SetAnchorAndMarginPreset(Control control, LayoutPreset preset,
LayoutPresetMode mode = LayoutPresetMode.MinSize,
int margin = 0)
{
SetAnchorPreset(control, preset);
SetMarginsPreset(control, preset, mode, margin);
}
/// <summary>
/// Changes all the anchors of a node at once to common presets.
/// The result is that the anchors are laid out to be suitable for a preset.
/// </summary>
/// <param name="preset">
/// The preset to apply to the anchors.
/// </param>
/// <param name="keepMargin">
/// If this is true, the control margin values themselves will not be changed,
/// and the control position and size will change according to the new anchor parameters.
/// If false, the control margins will adjust so that the control position and size remains the same relative to its parent.
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="preset" /> isn't a valid preset value.
/// </exception>
public static void SetAnchorPreset(Control control, LayoutPreset preset, bool keepMargin = false)
{
// TODO: Implement keepMargin.
// Left Anchor.
switch (preset)
{
case LayoutPreset.TopLeft:
case LayoutPreset.BottomLeft:
case LayoutPreset.CenterLeft:
case LayoutPreset.LeftWide:
case LayoutPreset.HorizontalCenterWide:
case LayoutPreset.Wide:
case LayoutPreset.TopWide:
case LayoutPreset.BottomWide:
control.SetValue(AnchorLeftProperty, 0f);
break;
case LayoutPreset.CenterTop:
case LayoutPreset.CenterBottom:
case LayoutPreset.Center:
case LayoutPreset.VerticalCenterWide:
control.SetValue(AnchorLeftProperty, 0.5f);
break;
case LayoutPreset.TopRight:
case LayoutPreset.BottomRight:
case LayoutPreset.CenterRight:
case LayoutPreset.RightWide:
control.SetValue(AnchorLeftProperty, 1f);
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
// Top Anchor.
switch (preset)
{
case LayoutPreset.TopLeft:
case LayoutPreset.TopRight:
case LayoutPreset.LeftWide:
case LayoutPreset.TopWide:
case LayoutPreset.Wide:
case LayoutPreset.RightWide:
case LayoutPreset.CenterTop:
case LayoutPreset.VerticalCenterWide:
control.SetValue(AnchorTopProperty, 0f);
break;
case LayoutPreset.CenterLeft:
case LayoutPreset.CenterRight:
case LayoutPreset.HorizontalCenterWide:
case LayoutPreset.Center:
control.SetValue(AnchorTopProperty, 0.5f);
break;
case LayoutPreset.CenterBottom:
case LayoutPreset.BottomLeft:
case LayoutPreset.BottomRight:
case LayoutPreset.BottomWide:
control.SetValue(AnchorTopProperty, 1f);
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
// Right Anchor.
switch (preset)
{
case LayoutPreset.TopLeft:
case LayoutPreset.CenterLeft:
case LayoutPreset.BottomLeft:
case LayoutPreset.LeftWide:
control.SetValue(AnchorRightProperty, 0f);
break;
case LayoutPreset.CenterTop:
case LayoutPreset.CenterBottom:
case LayoutPreset.Center:
case LayoutPreset.VerticalCenterWide:
control.SetValue(AnchorRightProperty, 0.5f);
break;
case LayoutPreset.CenterRight:
case LayoutPreset.TopRight:
case LayoutPreset.Wide:
case LayoutPreset.HorizontalCenterWide:
case LayoutPreset.TopWide:
case LayoutPreset.BottomWide:
case LayoutPreset.RightWide:
case LayoutPreset.BottomRight:
control.SetValue(AnchorRightProperty, 1f);
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
// Bottom Anchor.
switch (preset)
{
case LayoutPreset.TopWide:
case LayoutPreset.TopLeft:
case LayoutPreset.TopRight:
case LayoutPreset.CenterTop:
control.SetValue(AnchorBottomProperty, 0f);
break;
case LayoutPreset.CenterLeft:
case LayoutPreset.CenterRight:
case LayoutPreset.Center:
case LayoutPreset.HorizontalCenterWide:
control.SetValue(AnchorBottomProperty, 0.5f);
break;
case LayoutPreset.CenterBottom:
case LayoutPreset.BottomLeft:
case LayoutPreset.BottomRight:
case LayoutPreset.LeftWide:
case LayoutPreset.Wide:
case LayoutPreset.RightWide:
case LayoutPreset.VerticalCenterWide:
case LayoutPreset.BottomWide:
control.SetValue(AnchorBottomProperty, 1f);
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
}
/// <summary>
/// Changes all the margins of a control at once to common presets.
/// The result is that the control is laid out as specified by the preset.
///
/// Note that the current size and minimum size of the control affects how
/// each of the margins will be set, so if your control needs to shrink beyond its
/// current size / min size, you should either not call this method or only call it when your
/// control has a size of (0, 0). Otherwise your control's size will never be able
/// to go below the size implied by the margins set in this method.
/// </summary>
/// <param name="preset"></param>
/// <param name="resizeMode"></param>
/// <param name="margin">Some extra margin to add depending on the preset chosen.</param>
public static void SetMarginsPreset(Control control, LayoutPreset preset,
LayoutPresetMode resizeMode = LayoutPresetMode.MinSize,
int margin = 0)
{
control.Measure(Vector2Helpers.Infinity);
var newSize = control.Size;
var minSize = control.DesiredSize;
if ((resizeMode & LayoutPresetMode.KeepWidth) == 0)
{
newSize = new Vector2(minSize.X, newSize.Y);
}
if ((resizeMode & LayoutPresetMode.KeepHeight) == 0)
{
newSize = new Vector2(newSize.X, minSize.Y);
}
var parentSize = control.Parent?.Size ?? Vector2.Zero;
var anchorLeft = control.GetValue<float>(AnchorLeftProperty);
var anchorTop = control.GetValue<float>(AnchorTopProperty);
var anchorRight = control.GetValue<float>(AnchorRightProperty);
var anchorBottom = control.GetValue<float>(AnchorBottomProperty);
float marginLeft;
float marginTop;
float marginRight;
float marginBottom;
// Left Margin.
switch (preset)
{
case LayoutPreset.TopLeft:
case LayoutPreset.BottomLeft:
case LayoutPreset.CenterLeft:
case LayoutPreset.LeftWide:
case LayoutPreset.HorizontalCenterWide:
case LayoutPreset.Wide:
case LayoutPreset.TopWide:
case LayoutPreset.BottomWide:
// The AnchorLeft bit is to reverse the effect of anchors,
// So that the preset result is the same no matter what margins are set.
marginLeft = parentSize.X * (0 - anchorLeft) + margin;
break;
case LayoutPreset.CenterTop:
case LayoutPreset.CenterBottom:
case LayoutPreset.Center:
case LayoutPreset.VerticalCenterWide:
marginLeft = parentSize.X * (0.5f - anchorLeft) - newSize.X / 2;
break;
case LayoutPreset.TopRight:
case LayoutPreset.BottomRight:
case LayoutPreset.CenterRight:
case LayoutPreset.RightWide:
marginLeft = parentSize.X * (1 - anchorLeft) - newSize.X - margin;
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
// Top Anchor.
switch (preset)
{
case LayoutPreset.TopLeft:
case LayoutPreset.TopRight:
case LayoutPreset.LeftWide:
case LayoutPreset.TopWide:
case LayoutPreset.Wide:
case LayoutPreset.RightWide:
case LayoutPreset.CenterTop:
case LayoutPreset.VerticalCenterWide:
marginTop = parentSize.Y * (0 - anchorTop) + margin;
break;
case LayoutPreset.CenterLeft:
case LayoutPreset.CenterRight:
case LayoutPreset.HorizontalCenterWide:
case LayoutPreset.Center:
marginTop = parentSize.Y * (0.5f - anchorTop) - newSize.Y / 2;
break;
case LayoutPreset.CenterBottom:
case LayoutPreset.BottomLeft:
case LayoutPreset.BottomRight:
case LayoutPreset.BottomWide:
marginTop = parentSize.Y * (1 - anchorTop) - newSize.Y - margin;
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
// Right Anchor.
switch (preset)
{
case LayoutPreset.TopLeft:
case LayoutPreset.CenterLeft:
case LayoutPreset.BottomLeft:
case LayoutPreset.LeftWide:
marginRight = parentSize.X * (0 - anchorRight) + newSize.X + margin;
break;
case LayoutPreset.CenterTop:
case LayoutPreset.CenterBottom:
case LayoutPreset.Center:
case LayoutPreset.VerticalCenterWide:
marginRight = parentSize.X * (0.5f - anchorRight) + newSize.X;
break;
case LayoutPreset.CenterRight:
case LayoutPreset.TopRight:
case LayoutPreset.Wide:
case LayoutPreset.HorizontalCenterWide:
case LayoutPreset.TopWide:
case LayoutPreset.BottomWide:
case LayoutPreset.RightWide:
case LayoutPreset.BottomRight:
marginRight = parentSize.X * (1 - anchorRight) - margin;
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
// Bottom Anchor.
switch (preset)
{
case LayoutPreset.TopWide:
case LayoutPreset.TopLeft:
case LayoutPreset.TopRight:
case LayoutPreset.CenterTop:
marginBottom = parentSize.Y * (0 - anchorBottom) + newSize.Y + margin;
break;
case LayoutPreset.CenterLeft:
case LayoutPreset.CenterRight:
case LayoutPreset.Center:
case LayoutPreset.HorizontalCenterWide:
marginBottom = parentSize.Y * (0.5f - anchorBottom) + newSize.Y;
break;
case LayoutPreset.CenterBottom:
case LayoutPreset.BottomLeft:
case LayoutPreset.BottomRight:
case LayoutPreset.LeftWide:
case LayoutPreset.Wide:
case LayoutPreset.RightWide:
case LayoutPreset.VerticalCenterWide:
case LayoutPreset.BottomWide:
marginBottom = parentSize.Y * (1 - anchorBottom) - margin;
break;
default:
throw new ArgumentOutOfRangeException(nameof(preset), preset, null);
}
control.SetValue(MarginLeftProperty, marginLeft);
control.SetValue(MarginTopProperty, marginTop);
control.SetValue(MarginRightProperty, marginRight);
control.SetValue(MarginBottomProperty, marginBottom);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
var min = Vector2.Zero;
var uiScale = UIScale;
foreach (var child in Children)
{
var growH = child.GetValue<GrowDirection>(GrowHorizontalProperty);
var growV = child.GetValue<GrowDirection>(GrowVerticalProperty);
var anchorMargins = CalcAnchorMargins(availableSize, uiScale, child);
var size = availableSize;
if (growH == GrowDirection.Constrain)
size.X = anchorMargins.Width / uiScale;
if (growV == GrowDirection.Constrain)
size.Y = anchorMargins.Height / uiScale;
child.Measure(size);
min = Vector2.Max(min, child.DesiredSize);
}
return InheritChildMeasure ? min : Vector2.Zero;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
foreach (var child in Children)
{
child.Arrange(CalcChildRect(finalSize, UIScale, child, out _));
}
return finalSize;
}
protected internal override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (!Debug)
return;
var (pSizeX, pSizeY) = PixelSize;
foreach (var child in Children)
{
if (!child.GetValue(DebugProperty))
{
continue;
}
var rect = CalcChildRect(Size, UIScale, child, out var anchorSize);
var left = rect.Left * UIScale;
var right = rect.Right * UIScale;
var top = rect.Top * UIScale;
var bottom = rect.Bottom * UIScale;
DrawVLine(anchorSize.Left, Color.Pink);
DrawVLine(anchorSize.Right, Color.Green);
DrawHLine(anchorSize.Top, Color.Pink);
DrawHLine(anchorSize.Bottom, Color.Green);
/*
DrawVLine(left, Color.Orange);
DrawVLine(right, Color.Blue);
DrawHLine(top, Color.Orange);
DrawHLine(bottom, Color.Blue);
*/
handle.DrawRect(new UIBox2(left, top, right, bottom), Color.Red, false);
}
void DrawVLine(float x, Color color)
{
handle.DrawLine(new(x, 0), new(x, pSizeY), color);
}
void DrawHLine(float y, Color color)
{
handle.DrawLine(new(0, y), new(pSizeX, y), color);
}
}
private static UIBox2 CalcAnchorMargins(Vector2 ourSize, float uiScale, Control child)
{
var (pSizeX, pSizeY) = ourSize * uiScale;
var anchorLeft = child.GetValue<float>(AnchorLeftProperty);
var anchorTop = child.GetValue<float>(AnchorTopProperty);
var anchorRight = child.GetValue<float>(AnchorRightProperty);
var anchorBottom = child.GetValue<float>(AnchorBottomProperty);
var marginLeft = child.GetValue<float>(MarginLeftProperty) * uiScale;
var marginTop = child.GetValue<float>(MarginTopProperty) * uiScale;
var marginRight = child.GetValue<float>(MarginRightProperty) * uiScale;
var marginBottom = child.GetValue<float>(MarginBottomProperty) * uiScale;
var left = anchorLeft * pSizeX + marginLeft;
var top = anchorTop * pSizeY + marginTop;
var right = anchorRight * pSizeX + marginRight;
var bottom = anchorBottom * pSizeY + marginBottom;
// Yes, this can return boxes with left > right (and top > bottom).
// This is "intentional", see comment in CalcChildRect.
return new UIBox2(left, top, right, bottom);
}
private static UIBox2 CalcChildRect(Vector2 ourSize, float uiScale, Control child, out UIBox2 anchorSize)
{
// Calculate where the control "wants" to be by its anchors/margins.
var growHorizontal = child.GetValue<GrowDirection>(GrowHorizontalProperty);
var growVertical = child.GetValue<GrowDirection>(GrowVerticalProperty);
anchorSize = CalcAnchorMargins(ourSize, uiScale, child);
// This intentionally results in negatives if the right bound is < the left bound.
// Which then causes HandleLayoutOverflow to CORRECTLY work from the right bound instead.
var (wSizeX, wSizeY) = (anchorSize.Right - anchorSize.Left, anchorSize.Bottom - anchorSize.Top);
var (minSizeX, minSizeY) = child.DesiredPixelSize;
HandleLayoutOverflow(growHorizontal, minSizeX, anchorSize.Left, wSizeX, out var posX, out var sizeX);
HandleLayoutOverflow(growVertical, minSizeY, anchorSize.Top, wSizeY, out var posY, out var sizeY);
return UIBox2.FromDimensions(posX / uiScale, posY / uiScale, sizeX / uiScale, sizeY / uiScale);
}
private static void HandleLayoutOverflow(GrowDirection direction, float minSize, float wPos, float wSize,
out float pos,
out float size)
{
var overflow = minSize - wSize;
if (overflow <= 0 || direction == GrowDirection.Constrain)
{
pos = wPos;
size = wSize;
return;
}
switch (direction)
{
case GrowDirection.End:
pos = wPos;
break;
case GrowDirection.Begin:
pos = wPos - overflow;
break;
case GrowDirection.Both:
pos = wPos - overflow / 2;
break;
default:
throw new ArgumentOutOfRangeException();
}
size = minSize;
}
private static void LayoutPropertyChangedCallback(Control owner, AttachedPropertyChangedEventArgs eventArgs)
{
if (owner.Parent is LayoutContainer container)
{
container.InvalidateArrange();
}
}
/// <summary>
/// Controls how the control should move when its wanted size (controlled by anchors/margins) is smaller
/// than its minimum size.
/// </summary>
public enum GrowDirection : byte
{
/// <summary>
/// The control will expand to the bottom right to reach its minimum size.
/// </summary>
End = 0,
/// <summary>
/// The control will expand to the top left to reach its minimum size.
/// </summary>
Begin,
/// <summary>
/// The control will expand on all axes equally to reach its minimum size.
/// </summary>
Both,
/// <summary>
/// The control will not be allowed to grow on this axis.
/// </summary>
Constrain,
}
/// <seealso cref="Control.SetMarginsPreset" />
[Flags]
[PublicAPI]
public enum LayoutPresetMode : byte
{
/// <summary>
/// Reset control size to minimum size.
/// </summary>
MinSize = 0,
/// <summary>
/// Reset height to minimum but keep width the same.
/// </summary>
KeepWidth = 1,
/// <summary>
/// Reset width to minimum but keep height the same.
/// </summary>
KeepHeight = 2,
/// <summary>
/// Do not modify control size at all.
/// </summary>
KeepSize = KeepWidth | KeepHeight,
}
public enum LayoutPreset : byte
{
TopLeft = 0,
TopRight = 1,
BottomLeft = 2,
BottomRight = 3,
CenterLeft = 4,
CenterTop = 5,
CenterRight = 6,
CenterBottom = 7,
Center = 8,
LeftWide = 9,
TopWide = 10,
RightWide = 11,
BottomWide = 12,
VerticalCenterWide = 13,
HorizontalCenterWide = 14,
Wide = 15,
}
}
}