/* * 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 { /// /// A class which stores the results of a single grid layout calculation pass. /// internal sealed class GridLayoutResults { /// /// Builds a matrix of the components at each given location. Components only are /// entered at their origin cell (ignoring row and column span). /// /// The maximum number of rows. /// The maximum number of columns. /// The components to add. /// A 2-D array of the components at a given row/column location. private static ICollection[,] GetMatrix(int rows, int columns, ICollection components) { var spec = new ICollection[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(8); atLocation.Add(component); } } return spec; } /// /// The columns in the grid. /// public IList ColumnSpecs { get; } /// /// The components in the grid, in order of addition. /// public ICollection Components { get; } /// /// The number of columns in the grid. /// public int Columns { get; } /// /// The columns in the grid with their calculated widths. /// public IList ComputedColumnSpecs { get; } /// /// The rows in the grid with their calculated heights. /// public IList ComputedRowSpecs { get; } /// /// The minimum total height. /// public float MinHeight { get; private set; } /// /// The minimum total width. /// public float MinWidth { get; private set; } /// /// The components which were laid out. /// public ICollection[,] Matrix { get; } /// /// The rows in the grid. /// public IList RowSpecs { get; } /// /// The number of rows in the grid. /// public int Rows { get; } /// /// The total flexible height weights. /// public float TotalFlexHeight { get; private set; } /// /// The total flexible width weights. /// public float TotalFlexWidth { get; private set; } internal GridLayoutResults(IList rows, IList columns, ICollection> 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(Columns); ComputedRowSpecs = new List(Rows); TotalFlexHeight = TotalFlexWidth = 0.0f; // Populate alive components Components = new List(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); } /// /// Calculates the base height of each row, the minimum it gets before extra space /// is distributed. /// 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; } /// /// Calculates the base width of each row, the minimum it gets before extra space /// is distributed. /// 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; } /// /// For a multicolumn component, ratiometrically splits up any excess preferred size /// among the columns in its span that have a flexible width. /// /// The component to reallocate sizes. 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); } } /// /// For a multirow component, ratiometrically splits up any excess preferred size /// among the rows in its span that have a flexible height. /// /// The component to reallocate sizes. 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); } } /// /// Retrieves the preferred height of a cell. /// /// The cell's row. /// The cell's column. /// The preferred height. 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; } /// /// Retrieves the preferred width of a cell. /// /// The cell's row. /// The cell's column. /// The preferred width. 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; } } /// /// A component in the grid with its sizes computed. /// internal sealed class SizedGridComponent : GridComponentSpec { /// /// The object and its computed horizontal sizes. /// public LayoutSizes HorizontalSize { get; set; } /// /// The object and its computed vertical sizes. /// 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); } } }