using System; using System.Numerics; using Robust.Shared.Collections; using Robust.Shared.Maths; namespace Robust.Client.UserInterface.Controls; /// /// Lays of children sequentially, "wrapping" them onto another row/column if necessary. /// public sealed class WrapContainer : Container { /// /// Specifies the amount of space between two children, on the main axis. /// public const string StylePropertySeparation = "separation"; /// /// Specifies the amount of space between two children, on the cross axis. /// public const string StylePropertyCrossSeparation = "cross-separation"; // Parameters. private Axis _layoutAxis; private ItemJustification _justification; private bool _equalSize; private bool _reverse; private int? _separationOverride; private int? _crossSeparationOverride; // Cached layout data. private ValueList<(int endIndex, float cross)> _rowIndices; private float _lastMeasureCross; /// /// Specifies the amount of space between two children, on the main axis. /// /// /// This property overrides , if set. /// public int? SeparationOverride { get => _separationOverride; set { _separationOverride = value; InvalidateMeasure(); } } /// /// Specifies the amount of space between two children, on the cross axis. /// /// /// This property overrides , if set. /// public int? CrossSeparationOverride { get => _crossSeparationOverride; set { _crossSeparationOverride = value; InvalidateMeasure(); } } /// /// The along which to lay out children. /// public Axis LayoutAxis { get => _layoutAxis; set { _layoutAxis = value; InvalidateMeasure(); } } /// /// If true, all children will be laid out with the size of the largest child. /// public bool EqualSize { get => _equalSize; set { _equalSize = value; InvalidateMeasure(); } } /// /// Determines where items will be laid out on an individual row/column. /// public ItemJustification Justification { get => _justification; set { _justification = value; InvalidateArrange(); } } /// /// If true, reverses the order on which wrapping rows/columns are laid out. /// /// /// /// On horizontal axis, the first children are on the top row, and new rows are added downwards. /// With set to true, /// the first children are instead on the bottom row, with new rows growing upwards. /// /// public bool Reverse { get => _reverse; set { _reverse = value; InvalidateArrange(); } } private int ActualSeparation { get { if (SeparationOverride is not null) { return SeparationOverride.Value; } return StylePropertyDefault(StylePropertySeparation, 0); } } private int ActualCrossSeparation { get { if (CrossSeparationOverride is not null) { return CrossSeparationOverride.Value; } return StylePropertyDefault(StylePropertyCrossSeparation, 0); } } protected override Vector2 MeasureOverride(Vector2 availableSize) { var axis = LayoutAxis; return axis switch { Axis.Horizontal => MeasureImplementation(availableSize), Axis.HorizontalReverse => MeasureImplementation(availableSize), Axis.Vertical => MeasureImplementation(availableSize), Axis.VerticalReverse => MeasureImplementation(availableSize), _ => throw new ArgumentOutOfRangeException() }; } private Vector2 MeasureImplementation(Vector2 availableSize) where TAxis : IAxisImplementation { _rowIndices.Clear(); var realAvailableSize = availableSize; availableSize = TAxis.SizeToAxis(availableSize); // TODO: Round to pixels properly. var separation = ActualSeparation; var crossSeparation = ActualCrossSeparation; var curMainSize = 0f; var curCrossSize = 0f; var totalCrossSize = 0f; var maxMainSize = 0f; var firstOnRow = true; var equalDesiredSize = _equalSize ? TAxis.SizeToAxis(GetMaxMeasure(realAvailableSize)) : default; var countLaidOut = 0; foreach (var control in Children) { Vector2 controlDesiredSize; if (_equalSize) { controlDesiredSize = equalDesiredSize; } else { control.Measure(realAvailableSize); controlDesiredSize = TAxis.SizeToAxis(control.DesiredSize); } var controlMainSize = controlDesiredSize.X; var spaceTaken = controlMainSize + (firstOnRow ? 0 : separation); if (curMainSize + spaceTaken > availableSize.X) { // We've wrapped. RowEnd(lastRow: false); curMainSize = controlMainSize; } else { curMainSize += spaceTaken; } curCrossSize = Math.Max(curCrossSize, controlDesiredSize.Y); firstOnRow = false; countLaidOut += 1; } RowEnd(lastRow: true); _lastMeasureCross = totalCrossSize; return TAxis.SizeFromAxis(new Vector2(maxMainSize, totalCrossSize)); void RowEnd(bool lastRow) { maxMainSize = Math.Max(maxMainSize, curMainSize); totalCrossSize += curCrossSize; if (!lastRow) totalCrossSize += crossSeparation; _rowIndices.Add((countLaidOut, curCrossSize)); curCrossSize = 0; } } private Vector2 GetMaxDesired() { var vec = Vector2.Zero; foreach (var child in Children) { vec = Vector2.Max(vec, child.DesiredSize); } return vec; } private Vector2 GetMaxMeasure(Vector2 availableSize) { var vec = Vector2.Zero; foreach (var child in Children) { child.Measure(availableSize); vec = Vector2.Max(vec, child.DesiredSize); } return vec; } protected override Vector2 ArrangeOverride(Vector2 finalSize) { var axis = LayoutAxis; return axis switch { Axis.Horizontal => ArrangeImplementation(finalSize), Axis.HorizontalReverse => ArrangeImplementation(finalSize), Axis.Vertical => ArrangeImplementation(finalSize), Axis.VerticalReverse => ArrangeImplementation(finalSize), _ => throw new ArgumentOutOfRangeException() }; } private Vector2 ArrangeImplementation(Vector2 finalSize) where TAxis : IAxisImplementation { var realFinalSize = finalSize; finalSize = TAxis.SizeToAxis(finalSize); var separation = ActualSeparation; var crossSeparation = ActualCrossSeparation; var baseOffset = Reverse ? new Vector2(0, _lastMeasureCross) : Vector2.Zero; var fixedSize = _equalSize ? (Vector2?)GetMaxDesired() : null; var start = 0; for (var i = 0; i < _rowIndices.Count; i++) { var (endIndex, cross) = _rowIndices[i]; if (Reverse) { baseOffset.Y -= cross; } var box = TAxis.BoxFromAxis(UIBox2.FromDimensions(baseOffset, finalSize with { Y = cross }), realFinalSize); BoxContainer.LayOutItems( box.TopLeft, box.Size, (BoxContainer.AlignMode)_justification, Children, start, endIndex, separation, fixedSize); start = endIndex; if (Reverse) { baseOffset.Y -= crossSeparation; } else { baseOffset.Y = baseOffset.Y + cross + crossSeparation; } } return realFinalSize; } /// /// Justification values for . /// public enum ItemJustification : byte { // These MUST match the values in BoxContainer. Begin = BoxContainer.AlignMode.Begin, Center = BoxContainer.AlignMode.Center, End = BoxContainer.AlignMode.End } }