Files
RobustToolbox/Robust.Client/UserInterface/Controls/BoxContainer.cs
T

283 lines
9.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls
{
/// <summary>
/// A container that lays out its children sequentially.
/// </summary>
[Virtual]
public class BoxContainer : Container
{
private LayoutOrientation _orientation;
public const string StylePropertySeparation = "separation";
private const int DefaultSeparation = 0;
/// <summary>
/// Specifies the alignment of the controls <b>along the orientation axis.</b>
/// </summary>
/// <remarks>
/// This is along the orientation axis, not cross to it.
/// This means that if your orientation is vertical and you set this to center,
/// your controls will be laid out in the vertical <i>center</i> of the box control instead of the top.
/// </remarks>
public AlignMode Align { get; set; }
private bool Vertical => Orientation == LayoutOrientation.Vertical;
public LayoutOrientation Orientation
{
get => _orientation;
set
{
_orientation = value;
InvalidateMeasure();
}
}
private int ActualSeparation
{
get
{
if (TryGetStyleProperty(StylePropertySeparation, out int separation))
{
return separation;
}
return SeparationOverride ?? DefaultSeparation;
}
}
public int? SeparationOverride { get; set; }
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
// Account for separation.
var separation = ActualSeparation * (ChildCount - 1);
var desiredSize = Vector2.Zero;
if (Vertical)
{
desiredSize.Y += separation;
availableSize.Y = Math.Max(0, availableSize.Y - separation);
}
else
{
desiredSize.X += separation;
availableSize.X = Math.Max(0, availableSize.X - separation);
}
// First, we measure non-stretching children.
var stretching = new List<Control>();
float totalStretchRatio = 0;
foreach (var child in Children)
{
if (!child.Visible)
continue;
var stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
if (stretch)
{
totalStretchRatio += child.SizeFlagsStretchRatio;
stretching.Add(child);
continue;
}
child.Measure(availableSize);
if (Vertical)
{
desiredSize.Y += child.DesiredSize.Y;
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
availableSize.Y = Math.Max(0, availableSize.Y - child.DesiredSize.Y);
}
else
{
desiredSize.X += child.DesiredSize.X;
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
availableSize.X = Math.Max(0, availableSize.X - child.DesiredSize.X);
}
}
if (stretching.Count == 0)
return desiredSize;
// Then we measure stretching children
foreach (var child in stretching)
{
var size = availableSize;
if (Vertical)
{
size.Y *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.Y += child.DesiredSize.Y;
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
}
else
{
size.X *= child.SizeFlagsStretchRatio / totalStretchRatio;
child.Measure(size);
desiredSize.X += child.DesiredSize.X;
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
}
// TODO Maybe make BoxContainer.MeasureOverride more rigorous.
// This should check if size < desired size. If it is, treat child as non-stretching (see the code in
// ArrangeOverride). This requires remeasuring all stretching controls + the control that just became
// non-stretching. But the re-measured controls might then become smaller (e.g. rich text wrapping),
// leading to a recursion problem.
}
return desiredSize;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var separation = ActualSeparation;
var stretchAvail = Vertical ? finalSize.Y : finalSize.X;
stretchAvail -= separation * (ChildCount - 1);
stretchAvail = Math.Max(0, stretchAvail);
// Step one: figure out the sizes of all our children and whether they want to stretch.
var sizeList = new List<(Control control, float size, bool stretch)>(ChildCount);
var totalStretchRatio = 0f;
foreach (var child in Children)
{
if (!child.Visible)
continue;
bool stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
if (!stretch)
{
var size = Vertical ? child.DesiredSize.Y : child.DesiredSize.X;
size = Math.Clamp(size, 0, stretchAvail);
stretchAvail -= size;
sizeList.Add((child, size, false));
}
else
{
totalStretchRatio += child.SizeFlagsStretchRatio;
sizeList.Add((child, 0, true));
}
}
stretchAvail = Math.Max(0, stretchAvail);
// Step two: allocate space for all the stretchable children.
float offset = 0;
var anyStretch = totalStretchRatio > 0;
if (anyStretch)
{
// We will treat stretching children that fail to reach their desired size as non-stretching.
// This then requires all stretching children to be re-stretched
bool stretchAvailChanged = true;
while (stretchAvailChanged)
{
stretchAvailChanged = false;
for (var i = 0; i < sizeList.Count; i++)
{
var (control, _, stretch) = sizeList[i];
if (!stretch)
continue;
var share = stretchAvail * control.SizeFlagsStretchRatio / totalStretchRatio;
var desired = Vertical ? control.DesiredSize.Y : control.DesiredSize.X;
if (share >= desired)
{
sizeList[i] = (control, share, true);
continue;
}
// Insufficient space -> treat as non-stretching.
sizeList[i] = (control, Math.Min(stretchAvail, desired), false);
stretchAvail = Math.Max(0, stretchAvail - desired);
totalStretchRatio -= control.SizeFlagsStretchRatio;
stretchAvailChanged = true;
}
}
}
else
{
// No stretching children -> offset the children based on the alignment.
switch (Align)
{
case AlignMode.Begin:
break;
case AlignMode.Center:
offset = stretchAvail / 2;
break;
case AlignMode.End:
offset = stretchAvail;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
// Step three: actually lay them out one by one.
var first = true;
foreach (var (control, size, _) in sizeList)
{
if (!first)
{
offset += separation;
}
first = false;
UIBox2 targetBox;
if (Vertical)
{
targetBox = new UIBox2(0, offset, finalSize.X, offset + size);
}
else
{
targetBox = new UIBox2(offset, 0, offset + size, finalSize.Y);
}
control.Arrange(targetBox);
offset += size;
}
return finalSize;
}
public enum AlignMode : byte
{
/// <summary>
/// Controls are laid out from the begin of the box container.
/// </summary>
Begin = 0,
/// <summary>
/// Controls are laid out from the center of the box container.
/// </summary>
Center = 1,
/// <summary>
/// Controls are laid out from the end of the box container.
/// </summary>
End = 2
}
/// <summary>
/// Orientation for a box container.
/// </summary>
public enum LayoutOrientation : byte
{
/// <summary>
/// Controls are laid out horizontally, left to right.
/// </summary>
Horizontal,
/// <summary>
/// Controls are laid out vertically, top to bottom.
/// </summary>
Vertical
}
}
}