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

587 lines
23 KiB
C#

using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Maths;
namespace Robust.Client.UserInterface.Controls
{
/// <summary>
/// A container that lays out its children in a grid. Can define specific count of
/// rows or specific count of columns (not both), and will grow to fill in additional rows/columns within
/// that limit. Alternatively, can define a maximum width or height, and grid will
/// lay out elements (aligned in a grid pattern, not floated) within the defined limit.
/// </summary>
[Virtual]
public class GridContainer : Container
{
// limit - depending on mode, this is either rows or columns
private int _limitedDimensionCount = 1;
// virtual pixels
private float _limitSize;
private LimitType _limitType = LimitType.Count;
private Dimension _limitDimension = Dimension.Column;
/// <summary>
/// Indicates whether row or column count has been specified, and thus
/// how items will fill them out as they are added.
/// This is set depending on whether you have specified Columns or Rows.
/// </summary>
public Dimension LimitedDimension => _limitDimension;
/// <summary>
/// Opposite dimension of LimitedDimension
/// </summary>
public Dimension UnlimitedDimension => _limitDimension == Dimension.Column ? Dimension.Row : Dimension.Column;
/// <summary>
/// Indicates whether we are limiting based on an explicit number of rows or columns, or limiting
/// based on a defined max width or height.
/// </summary>
public LimitType LimitType => _limitType;
/// <summary>
/// The "normal" direction of expansion when the defined row or column limit is met
/// is right (for row-limited) and down (for column-limited),
/// this inverts that so the container expands in the opposite direction as elements are added.
/// </summary>
public bool ExpandBackwards
{
get => _expandBackwards;
set
{
_expandBackwards = value;
InvalidateArrange();
}
}
private bool _expandBackwards;
/// <summary>
/// The number of columns to organize the children into. Setting this puts this grid
/// into LimitMode.LimitColumns and LimitType.Count - items will be added to fill up the entire row, up to the defined
/// limit of columns, and then create a second row.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
/// <returns>specified limit if LimitMode.LimitColums, otherwise the number
/// of columns being used for the current amount of children.</returns>
public int Columns
{
get => GetCount(Dimension.Column);
set => SetCount(Dimension.Column, value);
}
/// <summary>
/// The number of rows to organize the children into. Setting this puts this grid
/// into LimitMode.LimitRows and LimitType.Count - items will be added to fill up the entire column, up to the defined
/// limit of rows, and then create a second column.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
/// <returns>specified limit if LimitMode.LimitRows, otherwise the number
/// of rows being used for the current number of children.</returns>
public int Rows
{
get => GetCount(Dimension.Row);
set => SetCount(Dimension.Row, value);
}
/// <summary>
/// The max width (in virtual pixels) the grid of elements can have. This dynamically determines
/// the number of columns based on the size of the elements. Setting this puts this grid
/// into LimitMode.LimitColumns and LimitType.Size. Items will be added to fill up the entire row, up to the defined
/// width, and then create a second row.
///
/// In the presence of unevenly-sized children,
/// rows will still have the same amount elements - the items are laid out in a grid pattern such
/// that they are all aligned, the height and width of each "cell" being determined by
/// the greatest min height and min width among the elements. In the presence of expanding elements,
/// their pre-expanded size will be used to determine the cell layout, then the elements expand within
/// the defined Control.Size
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
public float MaxGridWidth
{
set => SetMaxSize(Dimension.Column, value);
}
/// <summary>
/// The max height (in virtual pixels) the grid of elements can have. This dynamically determines
/// the number of rows based on the size of the elements. Setting this puts this grid
/// into LimitMode.LimitRows and LimitType.Size - items will be added to fill up the entire column, up to the defined
/// height, and then create a second column.
///
/// In the presence of unevenly-sized children,
/// columns will still have the same amount elements - the items are laid out in a grid pattern such
/// that they are all aligned, the height and width of each "cell" being determined by
/// the greatest min height and min width among the elements. In the presence of expanding elements,
/// their pre-expanded size will be used to determine the layout, then the elements expand within
/// the defined Control.Size
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if the value assigned is less than or equal to 0.
/// </exception>
public float MaxGridHeight
{
set => SetMaxSize(Dimension.Row, value);
}
private int? _vSeparationOverride;
[SuppressMessage("ReSharper", "StringLiteralTypo")]
public int? VSeparationOverride
{
get => _vSeparationOverride;
set => _vSeparationOverride = value;
}
private int? _hSeparationOverride;
[SuppressMessage("ReSharper", "StringLiteralTypo")]
public int? HSeparationOverride
{
get => _hSeparationOverride;
set => _hSeparationOverride = value;
}
private Vector2i Separations => (_hSeparationOverride ?? 4, _vSeparationOverride ?? 4);
private float GetLimitPixelSize()
{
return _limitSize * UIScale;
}
private int GetCount(Dimension forDimension)
{
if (_limitType == LimitType.Count)
{
if (forDimension == _limitDimension) return _limitedDimensionCount;
if (ChildCount == 0)
{
return 1;
}
var divisor = (_limitDimension == Dimension.Column ? Columns : Rows);
var div = ChildCount / divisor;
if (ChildCount % divisor != 0)
{
div += 1;
}
return div;
}
else
{
if (forDimension == _limitDimension) return CalculateLimitedCount();
if (ChildCount == 0)
{
return 1;
}
var divisor = CalculateLimitedCount();;
var div = ChildCount / divisor;
if (ChildCount % divisor != 0)
{
div += 1;
}
return div;
}
}
private void SetCount(Dimension forDimension, int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero.");
}
_limitDimension = forDimension;
_limitType = LimitType.Count;
_limitedDimensionCount = value;
InvalidateMeasure();
}
private void SetMaxSize(Dimension forDimension, float value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), value, "Value must be greater than zero.");
}
_limitDimension = forDimension;
_limitType = LimitType.Size;
_limitSize = value;
InvalidateMeasure();
}
/// <summary>
/// If columns (or width) are being limited, calculates how many columns
/// there should be.
/// </summary>
private int CalculateLimitedCount()
{
if (_limitType == LimitType.Count) return _limitedDimensionCount;
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
// but at the start of the method here we'll set those to what they actually are based
// on the limited dimension, which might involve swapping them.
// For the below convention, we pretend that columns (or width) have a limit defined, thus
// the amount of rows is not limited (unlimited).
// we convert all elements to "cells" of the same size so they will align,
// also converting to our pretend scenario of limited columns/width
var (cellWidthActual, cellHeightActual) = CellSize();
var (wSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
var cellWidth = _limitDimension == Dimension.Column ? cellWidthActual : cellHeightActual;
var wSep = _limitDimension == Dimension.Column ? wSepActual : hSepActual;
// calculate how many cells will fit into a given column without going over, accounting
// for additional wSep between each cell only if there's more than one
if (ChildCount == 0) return 1;
if ((2 * cellWidth + wSep) > GetLimitPixelSize())
{
return 1;
}
return Math.Min(ChildCount, (int) ((GetLimitPixelSize() + wSep) / (cellWidth + wSep)));
}
/// <summary>
/// Calculates the size of a "cell" in physical pixels, for use in LimitType.Size mode. This
/// is based on the maximum minheight / minwidth of each element.
/// </summary>
/// <returns></returns>
private Vector2i CellSize()
{
int maxMinWidth = -1;
int maxMinHeight = -1;
foreach (var child in Children)
{
var (minSizeX, minSizeY) = child.DesiredPixelSize;
maxMinWidth = Math.Max(maxMinWidth, minSizeX);
maxMinHeight = Math.Max(maxMinHeight, minSizeY);
}
return new Vector2i(maxMinWidth, maxMinHeight);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
// but at the start of the method here we'll set those to what they actually are based
// on the limited dimension, which might involve swapping them.
// For the below convention, we pretend that columns have a limit defined, thus
// the amount of rows is not limited (unlimited).
foreach (var child in Children)
{
// TODO: This is not really correct in any fucking way but I CBA to fix this properly.
child.Measure(availableSize);
}
var rows = GetCount(UnlimitedDimension);
var cols = GetCount(LimitedDimension);
var cellSize = CellSize();
Span<int> minColWidth = stackalloc int[cols];
Span<int> minRowHeight = stackalloc int[rows];
minColWidth.Fill(0);
minRowHeight.Fill(0);
var index = 0;
foreach (var child in Children)
{
if (!child.Visible)
{
index--;
continue;
}
var column = index % cols;
var row = index / cols;
// also converting here to our "pretend" scenario where columns have a limit defined.
// note if we are limiting by size rather than count, the size of each child is constant (cell size)
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.DesiredPixelSize : cellSize;
var minSizeX = _limitDimension == Dimension.Column ? minSizeXActual : minSizeYActual;
var minSizeY = _limitDimension == Dimension.Column ? minSizeYActual : minSizeXActual;
minColWidth[column] = Math.Max(minSizeX, minColWidth[column]);
minRowHeight[row] = Math.Max(minSizeY, minRowHeight[row]);
index += 1;
}
// converting here to our "pretend" scenario where columns have a limit defined
var (wSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
var wSep = _limitDimension == Dimension.Column ? wSepActual : hSepActual;
var hSep = _limitDimension == Dimension.Column ? hSepActual : wSepActual;
var minWidth = AccumSizes(minColWidth, wSep);
var minHeight = AccumSizes(minRowHeight, hSep);
// converting back from our pretend scenario where columns are limited
return new Vector2(
_limitDimension == Dimension.Column ? minWidth : minHeight,
_limitDimension == Dimension.Column ? minHeight : minWidth) / UIScale;
}
private static int AccumSizes(Span<int> sizes, int separator)
{
var totalSize = 0;
var first = true;
foreach (var size in sizes)
{
totalSize += size;
if (first)
{
first = false;
}
else
{
totalSize += separator;
}
}
return totalSize;
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
// to make it easier to read and visualize, we're just going to use the terms "x" and "y", width, and height,
// rows and cols,
// but at the start of the method here we'll set those to what they actually are based
// on the limited dimension, which might involve swapping them.
// For the below convention, we pretend that columns have a limit defined, thus
// the amount of rows is not limited (unlimited).
var rows = GetCount(UnlimitedDimension);
var cols = GetCount(LimitedDimension);
var cellSize = CellSize();
Span<int> minColWidth = stackalloc int[cols];
// Minimum lateral size of the unlimited dimension
// (i.e. width of columns, height of rows).
Span<int> minRowHeight = stackalloc int[rows];
// columns that are set to expand vertically
Span<bool> colExpand = stackalloc bool[cols];
// rows that are set to expand horizontally
Span<bool> rowExpand = stackalloc bool[rows];
minColWidth.Fill(0);
minRowHeight.Fill(0);
colExpand.Fill(false);
rowExpand.Fill(false);
// Get minSize and size flag expand of each column and row.
// All we need to apply the same logic BoxContainer does.
var index = 0;
foreach (var child in Children)
{
if (!child.Visible)
{
continue;
}
var column = index % cols;
var row = index / cols;
// converting here to our "pretend" scenario where columns have a limit defined
// note if we are limiting by size rather than count, the size of each child is constant (cell size)
var (minSizeXActual, minSizeYActual) = _limitType == LimitType.Count ? child.DesiredPixelSize : cellSize;
var minSizeX = _limitDimension == Dimension.Column ? minSizeXActual : minSizeYActual;
var minSizeY = _limitDimension == Dimension.Column ? minSizeYActual : minSizeXActual;
minColWidth[column] = Math.Max(minSizeX, minColWidth[column]);
minRowHeight[row] = Math.Max(minSizeY, minRowHeight[row]);
var colExpandFlag = _limitDimension == Dimension.Column
? child.HorizontalExpand
: child.VerticalExpand;
var rowExpandFlag = UnlimitedDimension == Dimension.Column
? child.HorizontalExpand
: child.VerticalExpand;
colExpand[column] = colExpand[column] || colExpandFlag;
rowExpand[row] = rowExpand[row] || rowExpandFlag;
index += 1;
}
// Basically now we just apply BoxContainer logic on rows and columns.
var stretchMinX = 0;
var stretchMinY = 0;
// We do not use stretch ratios because Godot doesn't,
// which makes sense since what happens if two things on the same column have a different stretch ratio?
// Maybe there's an answer for that but I'm too lazy to think of a concrete solution
// and it would make this code more complex so...
// pass.
var stretchCountX = 0;
var stretchCountY = 0;
for (var i = 0; i < minColWidth.Length; i++)
{
if (!colExpand[i])
{
stretchMinX += minColWidth[i];
}
else
{
stretchCountX++;
}
}
for (var i = 0; i < minRowHeight.Length; i++)
{
if (!rowExpand[i])
{
stretchMinY += minRowHeight[i];
}
else
{
stretchCountY++;
}
}
// converting here to our "pretend" scenario where columns have a limit defined
var (vSepActual, hSepActual) = (Vector2i) (Separations * UIScale);
var hSep = _limitDimension == Dimension.Column ? hSepActual : vSepActual;
var vSep = _limitDimension == Dimension.Column ? vSepActual : hSepActual;
var width = (_limitDimension == Dimension.Column ? finalSize.X : finalSize.Y) * UIScale;
var height = (_limitDimension == Dimension.Column ? finalSize.Y : finalSize.X) * UIScale;
var stretchMaxX = width - hSep * (cols - 1);
var stretchMaxY = height - vSep * (rows - 1);
var stretchAvailX = Math.Max(0, stretchMaxX - stretchMinX);
var stretchAvailY = Math.Max(0, stretchMaxY - stretchMinY);
for (var i = 0; i < minColWidth.Length; i++)
{
if (!colExpand[i])
{
continue;
}
minColWidth[i] = (int) (stretchAvailX / stretchCountX);
}
for (var i = 0; i < minRowHeight.Length; i++)
{
if (!rowExpand[i])
{
continue;
}
minRowHeight[i] = (int) (stretchAvailY / stretchCountY);
}
// Actually lay them out.
// if inverted, (in our pretend "columns are limited" scenario) we must calculate the final
// height (as height will vary depending on number of elements), and then
// go backwards, starting from the bottom and filling elements in upwards
var finalVOffset = 0;
if (ExpandBackwards)
{
// we have to iterate through the elements first to determine the height each
// row will end up having, as they can vary
index = 0;
for (var i = 0; i < ChildCount; i++, index++)
{
var child = GetChild(i);
if (!child.Visible)
{
index--;
continue;
}
var column = index % cols;
var row = index / cols;
if (column == 0)
{
// Just started a new row/col.
if (row != 0)
{
finalVOffset += vSep + minRowHeight[row - 1];
}
}
}
}
var hOffset = 0;
var vOffset = ExpandBackwards ? finalVOffset : 0;
index = 0;
for (var i = 0; i < ChildCount; i++, index++)
{
var child = GetChild(i);
if (!child.Visible)
{
index--;
continue;
}
var column = index % cols;
var row = index / cols;
if (column == 0)
{
// Just started a new row
hOffset = 0;
if (row != 0)
{
if (ExpandBackwards)
{
// every time we start a new row we actually decrease the voffset, we are filling
// in the up direction
vOffset -= vSep + minRowHeight[row - 1];
}
else
{
vOffset += vSep + minRowHeight[row - 1];
}
}
}
// converting back from our "pretend" scenario
var left = _limitDimension == Dimension.Column ? hOffset : vOffset;
var top = _limitDimension == Dimension.Column ? vOffset : hOffset;
var boxWidth = _limitDimension == Dimension.Column ? minColWidth[column] : minRowHeight[row];
var boxHeight = _limitDimension == Dimension.Column ? minRowHeight[row] : minColWidth[column];
var box = UIBox2i.FromDimensions(left, top, boxWidth, boxHeight);
child.ArrangePixel(box);
hOffset += minColWidth[column] + hSep;
}
return finalSize;
}
}
public enum Dimension : byte
{
Column,
Row
}
public enum LimitType : byte
{
/// <summary>
/// Defined number of rows or columns
/// </summary>
Count,
/// <summary>
/// Defined max width or height, inside of which the number of rows or columns
/// will be fit.
/// </summary>
Size
}
}