Files
RobustToolbox/Robust.Client/UserInterface/Controls/BoxContainer.cs
T
ThomasandGitHub 0dc7da78cf Invalidate the measure of BoxContainer's SeparationOverride (#6572)
InvalidateMeasure of SeparationOverride in BoxContainer

It currently doesn't trigger a new measure/arrange, but it should.
2026-05-19 15:21:28 +02:00

268 lines
9.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
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; }
public LayoutOrientation Orientation
{
get => _orientation;
set
{
_orientation = value;
InvalidateMeasure();
}
}
private int ActualSeparation =>
SeparationOverride ?? StylePropertyDefault(StylePropertySeparation, DefaultSeparation);
public int? SeparationOverride
{
get;
set
{
field = value;
InvalidateMeasure();
}
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (Orientation == LayoutOrientation.Vertical)
{
return MeasureItems<VerticalAxis>(availableSize);
}
else
{
return MeasureItems<HorizontalAxis>(availableSize);
}
}
private Vector2 MeasureItems<TAxis>(Vector2 availableSize) where TAxis : IAxisImplementation
{
availableSize = TAxis.SizeToAxis(availableSize);
// Account for separation.
var separation = ActualSeparation * (Children.Where(c => c.Visible).Count() - 1);
var desiredSize = new Vector2(separation, 0);
availableSize.X = Math.Max(0, availableSize.X) - separation;
// First, we measure non-stretching children.
foreach (var child in Children)
{
if (!child.Visible)
continue;
child.Measure(TAxis.SizeFromAxis(availableSize));
var childDesired = TAxis.SizeToAxis(child.DesiredSize);
desiredSize.X += childDesired.X;
desiredSize.Y = Math.Max(desiredSize.Y, childDesired.Y);
availableSize.X = Math.Max(0, availableSize.X - childDesired.X);
}
return TAxis.SizeFromAxis(desiredSize);
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
var separation = ActualSeparation;
if (Orientation == LayoutOrientation.Vertical)
{
LayOutItems<VerticalAxis>(default, finalSize, Align, Children, 0, ChildCount, separation);
}
else
{
LayOutItems<HorizontalAxis>(default, finalSize, Align, Children, 0, ChildCount, separation);
}
return finalSize;
}
internal static void LayOutItems<TAxis>(
Vector2 baseOffset,
Vector2 finalSize,
AlignMode align,
OrderedChildCollection children,
int start,
int end,
float separation,
Vector2? fixedSize = null)
where TAxis : IAxisImplementation
{
var realFinalSize = finalSize;
finalSize = TAxis.SizeToAxis(finalSize);
fixedSize = fixedSize == null ? null : TAxis.SizeToAxis(fixedSize.Value);
var visibleChildCount = 0;
for (var i = start; i < end; i++)
{
if (children[i].Visible)
visibleChildCount += 1;
}
var stretchAvail = finalSize.X;
stretchAvail -= separation * (visibleChildCount - 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)>(visibleChildCount);
var totalStretchRatio = 0f;
for (var i = start; i < end; i++)
{
var child = children[i];
if (!child.Visible)
continue;
bool stretch = TAxis.GetMainExpandFlag(child);
if (!stretch)
{
var measuredSize = fixedSize ?? TAxis.SizeToAxis(child.DesiredSize);
var size = measuredSize.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 measuredSize = fixedSize ?? TAxis.SizeToAxis(control.DesiredSize);
var desired = measuredSize.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;
var targetBox = TAxis.BoxFromAxis(new UIBox2(offset, 0, offset + size, finalSize.Y), realFinalSize);
targetBox = targetBox.Translated(baseOffset);
control.Arrange(targetBox);
offset += size;
}
}
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
}
}
}