mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-04 02:57:08 +02:00
283 lines
9.9 KiB
C#
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
|
|
}
|
|
}
|
|
}
|