/* * 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 PeterHan.PLib.Core; using PeterHan.PLib.UI.Layouts; using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace PeterHan.PLib.UI { /// /// Implements a flexible version of the base GridLayout. /// public sealed class PGridLayoutGroup : AbstractLayoutGroup { /// /// Calculates all column widths. /// /// The results from layout. /// The current container width. /// The margins within the borders. /// The column widths. private static float[] GetColumnWidths(GridLayoutResults results, float width, RectOffset margin) { int columns = results.Columns; // Find out how much flexible size can be given out float position = margin?.left ?? 0, right = margin?.right ?? 0; float actualWidth = width - position - right, totalFlex = results.TotalFlexWidth, excess = (totalFlex > 0.0f) ? (actualWidth - results.MinWidth) / totalFlex : 0.0f; float[] colX = new float[columns + 1]; // Determine start of columns for (int i = 0; i < columns; i++) { var spec = results.ComputedColumnSpecs[i]; colX[i] = position; position += spec.Width + spec.FlexWidth * excess; } colX[columns] = position; return colX; } /// /// Calculates all row heights. /// /// The results from layout. /// The current container height. /// The margins within the borders. /// The row heights. private static float[] GetRowHeights(GridLayoutResults results, float height, RectOffset margin) { int rows = results.Rows; // Find out how much flexible size can be given out float position = margin?.bottom ?? 0, top = margin?.top ?? 0; float actualWidth = height - position - top, totalFlex = results.TotalFlexHeight, excess = (totalFlex > 0.0f) ? (actualWidth - results.MinHeight) / totalFlex : 0.0f; float[] rowY = new float[rows + 1]; // Determine start of rows for (int i = 0; i < rows; i++) { var spec = results.ComputedRowSpecs[i]; rowY[i] = position; position += spec.Height + spec.FlexHeight * excess; } rowY[rows] = position; return rowY; } /// /// Calculates the final height of this component and applies it to the component. /// /// The component to calculate. /// The row locations from GetRowHeights. /// true if the height was applied, or false if the component was not laid out /// due to being disposed or set to ignore layout. private static bool SetFinalHeight(SizedGridComponent component, float[] rowY) { var margin = component.Margin; var sizes = component.VerticalSize; var target = sizes.source; bool ok = !sizes.ignore && target != null; if (ok) { int rows = rowY.Length - 1; // Clamp first and last row occupied by this object int first = component.Row, last = first + component.RowSpan; first = first.InRange(0, rows - 1); last = last.InRange(1, rows); // Align correctly in the cell box float y = rowY[first], rowHeight = rowY[last] - y; if (margin != null) { float border = margin.top + margin.bottom; y += margin.top; rowHeight -= border; sizes.min -= border; sizes.preferred -= border; } float actualHeight = PUIUtils.GetProperSize(sizes, rowHeight); // Take alignment into account y += PUIUtils.GetOffset(component.Alignment, PanelDirection.Vertical, rowHeight - actualHeight); target.rectTransform().SetInsetAndSizeFromParentEdge(RectTransform.Edge. Top, y, actualHeight); } return ok; } /// /// Calculates the final width of this component and applies it to the component. /// /// The component to calculate. /// The column locations from GetColumnWidths. /// true if the width was applied, or false if the component was not laid out /// due to being disposed or set to ignore layout. private static bool SetFinalWidth(SizedGridComponent component, float[] colX) { var margin = component.Margin; var sizes = component.HorizontalSize; var target = sizes.source; bool ok = !sizes.ignore && target != null; if (ok) { int columns = colX.Length - 1; // Clamp first and last column occupied by this object int first = component.Column, last = first + component.ColumnSpan; first = first.InRange(0, columns - 1); last = last.InRange(1, columns); // Align correctly in the cell box float x = colX[first], colWidth = colX[last] - x; if (margin != null) { float border = margin.left + margin.right; x += margin.left; colWidth -= border; sizes.min -= border; sizes.preferred -= border; } float actualWidth = PUIUtils.GetProperSize(sizes, colWidth); // Take alignment into account x += PUIUtils.GetOffset(component.Alignment, PanelDirection.Horizontal, colWidth - actualWidth); target.rectTransform().SetInsetAndSizeFromParentEdge(RectTransform.Edge. Left, x, actualWidth); } return ok; } /// /// The margin around the components as a whole. /// public RectOffset Margin { get { return margin; } set { margin = value; } } #pragma warning disable IDE0044 // Cannot be readonly for Unity serialization to work /// /// The children of this panel. /// [SerializeField] private IList> children; /// /// The columns in this panel. /// [SerializeField] private IList columns; /// /// The margin around the components as a whole. /// [SerializeField] private RectOffset margin; /// /// The current layout status. /// private GridLayoutResults results; /// /// The rows in this panel. /// [SerializeField] private IList rows; #pragma warning restore IDE0044 internal PGridLayoutGroup() { children = new List>(16); columns = new List(16); rows = new List(16); layoutPriority = 1; Margin = null; results = null; } /// /// Adds a column to this grid layout. /// /// The specification for that column. public void AddColumn(GridColumnSpec column) { if (column == null) throw new ArgumentNullException(nameof(column)); columns.Add(column); } /// /// Adds a component to this layout. Components added through other means to the /// transform will not be laid out at all! /// /// The child to add. /// The location where the child will be placed. public void AddComponent(GameObject child, GridComponentSpec spec) { if (child == null) throw new ArgumentNullException(nameof(child)); if (spec == null) throw new ArgumentNullException(nameof(spec)); children.Add(new GridComponent(spec, child)); child.SetParent(gameObject); } /// /// Adds a row to this grid layout. /// /// The specification for that row. public void AddRow(GridRowSpec row) { if (row == null) throw new ArgumentNullException(nameof(row)); rows.Add(row); } public override void CalculateLayoutInputHorizontal() { if (!locked) { results = new GridLayoutResults(rows, columns, children); var elements = ListPool.Allocate(); foreach (var component in results.Components) { // Cache size of children var obj = component.HorizontalSize.source; var margin = component.Margin; elements.Clear(); obj.GetComponents(elements); var sz = PUIUtils.CalcSizes(obj, PanelDirection.Horizontal, elements); if (!sz.ignore) { // Add borders int border = (margin == null) ? 0 : margin.left + margin.right; sz.min += border; sz.preferred += border; } component.HorizontalSize = sz; } elements.Recycle(); // Calculate columns sizes and our size results.CalcBaseWidths(); float width = results.MinWidth; if (Margin != null) width += Margin.left + Margin.right; minWidth = preferredWidth = width; flexibleWidth = (results.TotalFlexWidth > 0.0f) ? 1.0f : 0.0f; } } public override void CalculateLayoutInputVertical() { if (results != null && !locked) { var elements = ListPool.Allocate(); foreach (var component in results.Components) { // Cache size of children var obj = component.VerticalSize.source; var margin = component.Margin; elements.Clear(); obj.GetComponents(elements); var sz = PUIUtils.CalcSizes(obj, PanelDirection.Vertical, elements); if (!sz.ignore) { // Add borders int border = (margin == null) ? 0 : margin.top + margin.bottom; sz.min += border; sz.preferred += border; } component.VerticalSize = sz; } elements.Recycle(); // Calculate row sizes and our size results.CalcBaseHeights(); float height = results.MinHeight; if (Margin != null) height += Margin.bottom + Margin.top; minHeight = preferredHeight = height; flexibleHeight = (results.TotalFlexHeight > 0.0f) ? 1.0f : 0.0f; } } protected override void OnDisable() { base.OnDisable(); results = null; } protected override void OnEnable() { base.OnEnable(); results = null; } public override void SetLayoutHorizontal() { var obj = gameObject; if (results != null && obj != null && results.Columns > 0 && !locked) { float[] colX = GetColumnWidths(results, rectTransform.rect.width, Margin); // All components lay out var controllers = ListPool.Allocate(); foreach (var component in results.Components) if (SetFinalWidth(component, colX)) { // Lay out all children controllers.Clear(); component.HorizontalSize.source.GetComponents(controllers); foreach (var controller in controllers) controller.SetLayoutHorizontal(); } controllers.Recycle(); } } public override void SetLayoutVertical() { var obj = gameObject; if (results != null && obj != null && results.Rows > 0 && !locked) { float[] rowY = GetRowHeights(results, rectTransform.rect.height, Margin); // All components lay out var controllers = ListPool.Allocate(); foreach (var component in results.Components) if (!SetFinalHeight(component, rowY)) { // Lay out all children controllers.Clear(); component.VerticalSize.source.GetComponents(controllers); foreach (var controller in controllers) controller.SetLayoutVertical(); } controllers.Recycle(); } } } }