mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-01 17:47:24 +02:00
InvalidateMeasure of SeparationOverride in BoxContainer It currently doesn't trigger a new measure/arrange, but it should.
268 lines
9.0 KiB
C#
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
|
|
}
|
|
}
|
|
}
|