350 lines
12 KiB
C#
350 lines
12 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 PeterHan.PLib.Core;
|
|
using PeterHan.PLib.UI.Layouts;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace PeterHan.PLib.UI {
|
|
/// <summary>
|
|
/// Implements a flexible version of the base GridLayout.
|
|
/// </summary>
|
|
public sealed class PGridLayoutGroup : AbstractLayoutGroup {
|
|
/// <summary>
|
|
/// Calculates all column widths.
|
|
/// </summary>
|
|
/// <param name="results">The results from layout.</param>
|
|
/// <param name="width">The current container width.</param>
|
|
/// <param name="margin">The margins within the borders.</param>
|
|
/// <returns>The column widths.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates all row heights.
|
|
/// </summary>
|
|
/// <param name="results">The results from layout.</param>
|
|
/// <param name="height">The current container height.</param>
|
|
/// <param name="margin">The margins within the borders.</param>
|
|
/// <returns>The row heights.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the final height of this component and applies it to the component.
|
|
/// </summary>
|
|
/// <param name="component">The component to calculate.</param>
|
|
/// <param name="rowY">The row locations from GetRowHeights.</param>
|
|
/// <returns>true if the height was applied, or false if the component was not laid out
|
|
/// due to being disposed or set to ignore layout.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the final width of this component and applies it to the component.
|
|
/// </summary>
|
|
/// <param name="component">The component to calculate.</param>
|
|
/// <param name="colX">The column locations from GetColumnWidths.</param>
|
|
/// <returns>true if the width was applied, or false if the component was not laid out
|
|
/// due to being disposed or set to ignore layout.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The margin around the components as a whole.
|
|
/// </summary>
|
|
public RectOffset Margin {
|
|
get {
|
|
return margin;
|
|
}
|
|
set {
|
|
margin = value;
|
|
}
|
|
}
|
|
|
|
#pragma warning disable IDE0044 // Cannot be readonly for Unity serialization to work
|
|
/// <summary>
|
|
/// The children of this panel.
|
|
/// </summary>
|
|
[SerializeField]
|
|
private IList<GridComponent<GameObject>> children;
|
|
|
|
/// <summary>
|
|
/// The columns in this panel.
|
|
/// </summary>
|
|
[SerializeField]
|
|
private IList<GridColumnSpec> columns;
|
|
|
|
/// <summary>
|
|
/// The margin around the components as a whole.
|
|
/// </summary>
|
|
[SerializeField]
|
|
private RectOffset margin;
|
|
|
|
/// <summary>
|
|
/// The current layout status.
|
|
/// </summary>
|
|
private GridLayoutResults results;
|
|
|
|
/// <summary>
|
|
/// The rows in this panel.
|
|
/// </summary>
|
|
[SerializeField]
|
|
private IList<GridRowSpec> rows;
|
|
#pragma warning restore IDE0044
|
|
|
|
internal PGridLayoutGroup() {
|
|
children = new List<GridComponent<GameObject>>(16);
|
|
columns = new List<GridColumnSpec>(16);
|
|
rows = new List<GridRowSpec>(16);
|
|
layoutPriority = 1;
|
|
Margin = null;
|
|
results = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a column to this grid layout.
|
|
/// </summary>
|
|
/// <param name="column">The specification for that column.</param>
|
|
public void AddColumn(GridColumnSpec column) {
|
|
if (column == null)
|
|
throw new ArgumentNullException(nameof(column));
|
|
columns.Add(column);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a component to this layout. Components added through other means to the
|
|
/// transform will not be laid out at all!
|
|
/// </summary>
|
|
/// <param name="child">The child to add.</param>
|
|
/// <param name="spec">The location where the child will be placed.</param>
|
|
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<GameObject>(spec, child));
|
|
child.SetParent(gameObject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a row to this grid layout.
|
|
/// </summary>
|
|
/// <param name="row">The specification for that row.</param>
|
|
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<Component, PGridLayoutGroup>.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<Component, PGridLayoutGroup>.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<ILayoutController, PGridLayoutGroup>.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<ILayoutController, PGridLayoutGroup>.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();
|
|
}
|
|
}
|
|
}
|
|
}
|