305 lines
10 KiB
C#
305 lines
10 KiB
C#
/*
|
|
* Copyright 2022 Peter Han
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
* and associated documentation files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all copies or
|
|
* substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace PeterHan.PLib.UI.Layouts {
|
|
/// <summary>
|
|
/// A class which stores the results of a single grid layout calculation pass.
|
|
/// </summary>
|
|
internal sealed class GridLayoutResults {
|
|
/// <summary>
|
|
/// Builds a matrix of the components at each given location. Components only are
|
|
/// entered at their origin cell (ignoring row and column span).
|
|
/// </summary>
|
|
/// <param name="rows">The maximum number of rows.</param>
|
|
/// <param name="columns">The maximum number of columns.</param>
|
|
/// <param name="components">The components to add.</param>
|
|
/// <returns>A 2-D array of the components at a given row/column location.</returns>
|
|
private static ICollection<SizedGridComponent>[,] GetMatrix(int rows, int columns,
|
|
ICollection<SizedGridComponent> components) {
|
|
var spec = new ICollection<SizedGridComponent>[rows, columns];
|
|
foreach (var component in components) {
|
|
int x = component.Row, y = component.Column;
|
|
if (x >= 0 && x < rows && y >= 0 && y < columns) {
|
|
// Multiple components are allowed at a given cell
|
|
var atLocation = spec[x, y];
|
|
if (atLocation == null)
|
|
spec[x, y] = atLocation = new List<SizedGridComponent>(8);
|
|
atLocation.Add(component);
|
|
}
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The columns in the grid.
|
|
/// </summary>
|
|
public IList<GridColumnSpec> ColumnSpecs { get; }
|
|
|
|
/// <summary>
|
|
/// The components in the grid, in order of addition.
|
|
/// </summary>
|
|
public ICollection<SizedGridComponent> Components { get; }
|
|
|
|
/// <summary>
|
|
/// The number of columns in the grid.
|
|
/// </summary>
|
|
public int Columns { get; }
|
|
|
|
/// <summary>
|
|
/// The columns in the grid with their calculated widths.
|
|
/// </summary>
|
|
public IList<GridColumnSpec> ComputedColumnSpecs { get; }
|
|
|
|
/// <summary>
|
|
/// The rows in the grid with their calculated heights.
|
|
/// </summary>
|
|
public IList<GridRowSpec> ComputedRowSpecs { get; }
|
|
|
|
/// <summary>
|
|
/// The minimum total height.
|
|
/// </summary>
|
|
public float MinHeight { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The minimum total width.
|
|
/// </summary>
|
|
public float MinWidth { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The components which were laid out.
|
|
/// </summary>
|
|
public ICollection<SizedGridComponent>[,] Matrix { get; }
|
|
|
|
/// <summary>
|
|
/// The rows in the grid.
|
|
/// </summary>
|
|
public IList<GridRowSpec> RowSpecs { get; }
|
|
|
|
/// <summary>
|
|
/// The number of rows in the grid.
|
|
/// </summary>
|
|
public int Rows { get; }
|
|
|
|
/// <summary>
|
|
/// The total flexible height weights.
|
|
/// </summary>
|
|
public float TotalFlexHeight { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The total flexible width weights.
|
|
/// </summary>
|
|
public float TotalFlexWidth { get; private set; }
|
|
|
|
internal GridLayoutResults(IList<GridRowSpec> rows, IList<GridColumnSpec> columns,
|
|
ICollection<GridComponent<GameObject>> components) {
|
|
if (rows == null)
|
|
throw new ArgumentNullException(nameof(rows));
|
|
if (columns == null)
|
|
throw new ArgumentNullException(nameof(columns));
|
|
if (components == null)
|
|
throw new ArgumentNullException(nameof(components));
|
|
Columns = columns.Count;
|
|
Rows = rows.Count;
|
|
ColumnSpecs = columns;
|
|
MinHeight = MinWidth = 0.0f;
|
|
RowSpecs = rows;
|
|
ComputedColumnSpecs = new List<GridColumnSpec>(Columns);
|
|
ComputedRowSpecs = new List<GridRowSpec>(Rows);
|
|
TotalFlexHeight = TotalFlexWidth = 0.0f;
|
|
// Populate alive components
|
|
Components = new List<SizedGridComponent>(Math.Max(components.Count, 4));
|
|
foreach (var component in components) {
|
|
var item = component.Item;
|
|
if (item != null)
|
|
Components.Add(new SizedGridComponent(component, item));
|
|
}
|
|
Matrix = GetMatrix(Rows, Columns, Components);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the base height of each row, the minimum it gets before extra space
|
|
/// is distributed.
|
|
/// </summary>
|
|
internal void CalcBaseHeights() {
|
|
int rows = Rows;
|
|
MinHeight = TotalFlexHeight = 0.0f;
|
|
ComputedRowSpecs.Clear();
|
|
for (int row = 0; row < rows; row++) {
|
|
var spec = RowSpecs[row];
|
|
float height = spec.Height, flex = spec.FlexHeight;
|
|
if (height <= 0.0f)
|
|
// Auto height
|
|
for (int i = 0; i < Columns; i++)
|
|
height = Math.Max(height, PreferredHeightAt(row, i));
|
|
if (flex > 0.0f)
|
|
TotalFlexHeight += flex;
|
|
ComputedRowSpecs.Add(new GridRowSpec(height, flex));
|
|
}
|
|
foreach (var component in Components)
|
|
if (component.RowSpan > 1)
|
|
ExpandMultiRow(component);
|
|
// Min height is calculated after all multirow components are distributed
|
|
for (int row = 0; row < rows; row++)
|
|
MinHeight += ComputedRowSpecs[row].Height;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the base width of each row, the minimum it gets before extra space
|
|
/// is distributed.
|
|
/// </summary>
|
|
internal void CalcBaseWidths() {
|
|
int columns = Columns;
|
|
MinWidth = TotalFlexWidth = 0.0f;
|
|
ComputedColumnSpecs.Clear();
|
|
for (int column = 0; column < columns; column++) {
|
|
var spec = ColumnSpecs[column];
|
|
float width = spec.Width, flex = spec.FlexWidth;
|
|
if (width <= 0.0f)
|
|
// Auto width
|
|
for (int i = 0; i < Rows; i++)
|
|
width = Math.Max(width, PreferredWidthAt(i, column));
|
|
if (flex > 0.0f)
|
|
TotalFlexWidth += flex;
|
|
ComputedColumnSpecs.Add(new GridColumnSpec(width, flex));
|
|
}
|
|
foreach (var component in Components)
|
|
if (component.ColumnSpan > 1)
|
|
ExpandMultiColumn(component);
|
|
// Min width is calculated after all multicolumn components are distributed
|
|
for (int column = 0; column < columns; column++)
|
|
MinWidth += ComputedColumnSpecs[column].Width;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For a multicolumn component, ratiometrically splits up any excess preferred size
|
|
/// among the columns in its span that have a flexible width.
|
|
/// </summary>
|
|
/// <param name="component">The component to reallocate sizes.</param>
|
|
private void ExpandMultiColumn(SizedGridComponent component) {
|
|
float need = component.HorizontalSize.preferred, totalFlex = 0.0f;
|
|
int start = component.Column, end = start + component.ColumnSpan;
|
|
for (int i = start; i < end; i++) {
|
|
var spec = ComputedColumnSpecs[i];
|
|
if (spec.FlexWidth > 0.0f)
|
|
totalFlex += spec.FlexWidth;
|
|
need -= spec.Width;
|
|
}
|
|
if (need > 0.0f && totalFlex > 0.0f)
|
|
// No flex = we can do nothing about it
|
|
for (int i = start; i < end; i++) {
|
|
var spec = ComputedColumnSpecs[i];
|
|
float flex = spec.FlexWidth;
|
|
if (flex > 0.0f)
|
|
ComputedColumnSpecs[i] = new GridColumnSpec(spec.Width + flex * need /
|
|
totalFlex, flex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// For a multirow component, ratiometrically splits up any excess preferred size
|
|
/// among the rows in its span that have a flexible height.
|
|
/// </summary>
|
|
/// <param name="component">The component to reallocate sizes.</param>
|
|
private void ExpandMultiRow(SizedGridComponent component) {
|
|
float need = component.VerticalSize.preferred, totalFlex = 0.0f;
|
|
int start = component.Row, end = start + component.RowSpan;
|
|
for (int i = start; i < end; i++) {
|
|
var spec = ComputedRowSpecs[i];
|
|
if (spec.FlexHeight > 0.0f)
|
|
totalFlex += spec.FlexHeight;
|
|
need -= spec.Height;
|
|
}
|
|
if (need > 0.0f && totalFlex > 0.0f)
|
|
// No flex = we can do nothing about it
|
|
for (int i = start; i < end; i++) {
|
|
var spec = ComputedRowSpecs[i];
|
|
float flex = spec.FlexHeight;
|
|
if (flex > 0.0f)
|
|
ComputedRowSpecs[i] = new GridRowSpec(spec.Height + flex * need /
|
|
totalFlex, flex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the preferred height of a cell.
|
|
/// </summary>
|
|
/// <param name="row">The cell's row.</param>
|
|
/// <param name="column">The cell's column.</param>
|
|
/// <returns>The preferred height.</returns>
|
|
private float PreferredHeightAt(int row, int column) {
|
|
float size = 0.0f;
|
|
var atLocation = Matrix[row, column];
|
|
if (atLocation != null && atLocation.Count > 0)
|
|
foreach (var component in atLocation) {
|
|
var sizes = component.VerticalSize;
|
|
if (component.RowSpan < 2)
|
|
size = Math.Max(size, sizes.preferred);
|
|
}
|
|
return size;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the preferred width of a cell.
|
|
/// </summary>
|
|
/// <param name="row">The cell's row.</param>
|
|
/// <param name="column">The cell's column.</param>
|
|
/// <returns>The preferred width.</returns>
|
|
private float PreferredWidthAt(int row, int column) {
|
|
float size = 0.0f;
|
|
var atLocation = Matrix[row, column];
|
|
if (atLocation != null && atLocation.Count > 0)
|
|
foreach (var component in atLocation) {
|
|
var sizes = component.HorizontalSize;
|
|
if (component.ColumnSpan < 2)
|
|
size = Math.Max(size, sizes.preferred);
|
|
}
|
|
return size;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A component in the grid with its sizes computed.
|
|
/// </summary>
|
|
internal sealed class SizedGridComponent : GridComponentSpec {
|
|
/// <summary>
|
|
/// The object and its computed horizontal sizes.
|
|
/// </summary>
|
|
public LayoutSizes HorizontalSize { get; set; }
|
|
|
|
/// <summary>
|
|
/// The object and its computed vertical sizes.
|
|
/// </summary>
|
|
public LayoutSizes VerticalSize { get; set; }
|
|
|
|
internal SizedGridComponent(GridComponentSpec spec, GameObject item) {
|
|
Alignment = spec.Alignment;
|
|
Column = spec.Column;
|
|
ColumnSpan = spec.ColumnSpan;
|
|
Margin = spec.Margin;
|
|
Row = spec.Row;
|
|
RowSpan = spec.RowSpan;
|
|
HorizontalSize = new LayoutSizes(item);
|
|
VerticalSize = new LayoutSizes(item);
|
|
}
|
|
}
|
|
}
|