oni-priority-ux/mod/PLibUI/Layouts/RelativeLayoutUtil.cs

402 lines
16 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 System.Text;
using UnityEngine;
using UnityEngine.UI;
using EdgeStatus = PeterHan.PLib.UI.Layouts.RelativeLayoutParams.EdgeStatus;
namespace PeterHan.PLib.UI.Layouts {
/// <summary>
/// A helper class for RelativeLayout.
/// </summary>
internal static class RelativeLayoutUtil {
/// <summary>
/// Initializes and computes horizontal sizes for the components in this relative
/// layout.
/// </summary>
/// <param name="children">The location to store information about these components.</param>
/// <param name="all">The components to lay out.</param>
/// <param name="constraints">The constraints defined for these components.</param>
internal static void CalcX(this ICollection<RelativeLayoutResults> children,
RectTransform all, IDictionary<GameObject, RelativeLayoutParams> constraints) {
var comps = ListPool<Component, RelativeLayoutGroup>.Allocate();
var paramMap = DictionaryPool<GameObject, RelativeLayoutResults,
RelativeLayoutGroup>.Allocate();
int n = all.childCount;
children.Clear();
for (int i = 0; i < n; i++) {
var child = all.GetChild(i)?.gameObject;
if (child != null) {
comps.Clear();
// Calculate the preferred size using all layout components
child.GetComponents(comps);
var horiz = PUIUtils.CalcSizes(child, PanelDirection.Horizontal, comps);
if (!horiz.ignore) {
RelativeLayoutResults ip;
float w = horiz.preferred;
if (constraints.TryGetValue(child, out RelativeLayoutParams cons)) {
ip = new RelativeLayoutResults(child.rectTransform(), cons);
// Set override size by axis if necessary
var overrideSize = ip.OverrideSize;
if (overrideSize.x > 0.0f)
w = overrideSize.x;
paramMap[child] = ip;
} else
// Default its layout to fill all
ip = new RelativeLayoutResults(child.rectTransform(), null);
ip.PreferredWidth = w;
children.Add(ip);
}
}
}
// Resolve object references to other children
foreach (var ip in children) {
ip.TopParams = InitResolve(ip.TopEdge, paramMap);
ip.BottomParams = InitResolve(ip.BottomEdge, paramMap);
ip.LeftParams = InitResolve(ip.LeftEdge, paramMap);
ip.RightParams = InitResolve(ip.RightEdge, paramMap);
// All of these will die simultaneously when the list is recycled
}
paramMap.Recycle();
comps.Recycle();
}
/// <summary>
/// Computes vertical sizes for the components in this relative layout.
/// </summary>
/// <param name="children">The location to store information about these components.</param>
internal static void CalcY(this ICollection<RelativeLayoutResults> children) {
var comps = ListPool<Component, RelativeLayoutGroup>.Allocate();
foreach (var ip in children) {
var child = ip.Transform.gameObject;
var overrideSize = ip.OverrideSize;
comps.Clear();
// Calculate the preferred size using all layout components
child.gameObject.GetComponents(comps);
float h = PUIUtils.CalcSizes(child, PanelDirection.Vertical, comps).preferred;
// Set override size by axis if necessary
if (overrideSize.y > 0.0f)
h = overrideSize.y;
ip.PreferredHeight = h;
}
comps.Recycle();
}
/// <summary>
/// Calculates the minimum size the component must be to support a specific child
/// component.
/// </summary>
/// <param name="min">The lower edge constraint.</param>
/// <param name="max">The upper edge constraint.</param>
/// <param name="effective">The component size in that dimension plus margins.</param>
/// <returns>The minimum parent component size to fit the child.</returns>
internal static float ElbowRoom(EdgeStatus min, EdgeStatus max, float effective) {
float aMin = min.FromAnchor, aMax = max.FromAnchor, result, offMin = min.Offset,
offMax = max.Offset;
if (aMax > aMin)
// "Elbow room" method
result = (effective + offMin - offMax) / (aMax - aMin);
else
// Anchors are together
result = Math.Max(effective, Math.Max(Math.Abs(offMin), Math.Abs(offMax)));
return result;
}
/// <summary>
/// Executes the horizontal layout.
/// </summary>
/// <param name="children">The components to lay out.</param>
/// <param name="scratch">The location where components will be temporarily stored.</param>
/// <param name="mLeft">The left margin.</param>
/// <param name="mRight">The right margin.</param>
internal static void ExecuteX(this IEnumerable<RelativeLayoutResults> children,
List<ILayoutController> scratch, float mLeft = 0.0f, float mRight = 0.0f) {
foreach (var child in children) {
var rt = child.Transform;
var insets = child.Insets;
EdgeStatus l = child.LeftEdge, r = child.RightEdge;
// Set corner positions
rt.anchorMin = new Vector2(l.FromAnchor, 0.0f);
rt.anchorMax = new Vector2(r.FromAnchor, 1.0f);
// If anchored by pivot, resize with current anchors
if (child.UseSizeDeltaX)
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, child.
PreferredWidth);
else {
// Left
rt.offsetMin = new Vector2(l.Offset + insets.left + (l.FromAnchor <= 0.0f ?
mLeft : 0.0f), rt.offsetMin.y);
// Right
rt.offsetMax = new Vector2(r.Offset - insets.right - (r.FromAnchor >=
1.0f ? mRight : 0.0f), rt.offsetMax.y);
}
// Execute layout controllers if present
scratch.Clear();
rt.gameObject.GetComponents(scratch);
foreach (var component in scratch)
component.SetLayoutHorizontal();
}
}
/// <summary>
/// Executes the vertical layout.
/// </summary>
/// <param name="children">The components to lay out.</param>
/// <param name="scratch">The location where components will be temporarily stored.</param>
/// <param name="mBottom">The bottom margin.</param>
/// <param name="mTop">The top margin.</param>
internal static void ExecuteY(this IEnumerable<RelativeLayoutResults> children,
List<ILayoutController> scratch, float mBottom = 0.0f, float mTop = 0.0f) {
foreach (var child in children) {
var rt = child.Transform;
var insets = child.Insets;
EdgeStatus t = child.TopEdge, b = child.BottomEdge;
// Set corner positions
rt.anchorMin = new Vector2(rt.anchorMin.x, b.FromAnchor);
rt.anchorMax = new Vector2(rt.anchorMax.x, t.FromAnchor);
// If anchored by pivot, resize with current anchors
if (child.UseSizeDeltaY)
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, child.
PreferredHeight);
else {
// Bottom
rt.offsetMin = new Vector2(rt.offsetMin.x, b.Offset + insets.bottom +
(b.FromAnchor <= 0.0f ? mBottom : 0.0f));
// Top
rt.offsetMax = new Vector2(rt.offsetMax.x, t.Offset - insets.top -
(t.FromAnchor >= 1.0f ? mTop : 0.0f));
}
// Execute layout controllers if present
scratch.Clear();
rt.gameObject.GetComponents(scratch);
foreach (var component in scratch)
component.SetLayoutVertical();
}
}
/// <summary>
/// Calculates the minimum size in the X direction.
/// </summary>
/// <param name="children">The components to lay out.</param>
/// <returns>The minimum horizontal size.</returns>
internal static float GetMinSizeX(this IEnumerable<RelativeLayoutResults> children) {
float maxWidth = 0.0f;
foreach (var child in children) {
float width = ElbowRoom(child.LeftEdge, child.RightEdge, child.EffectiveWidth);
if (width > maxWidth) maxWidth = width;
}
return maxWidth;
}
/// <summary>
/// Calculates the minimum size in the Y direction.
/// </summary>
/// <param name="children">The components to lay out.</param>
/// <returns>The minimum vertical size.</returns>
internal static float GetMinSizeY(this IEnumerable<RelativeLayoutResults> children) {
float maxHeight = 0.0f;
foreach (var child in children) {
float height = ElbowRoom(child.BottomEdge, child.TopEdge, child.
EffectiveHeight);
if (height > maxHeight) maxHeight = height;
}
return maxHeight;
}
/// <summary>
/// Resolves a component reference if needed.
/// </summary>
/// <param name="edge">The edge to resolve.</param>
/// <param name="lookup">The location where the component can be looked up.</param>
/// <returns>The linked parameters for that edge if needed.</returns>
private static RelativeLayoutResults InitResolve(EdgeStatus edge,
IDictionary<GameObject, RelativeLayoutResults> lookup) {
RelativeLayoutResults result = null;
if (edge.Constraint == RelativeConstraintType.ToComponent)
if (!lookup.TryGetValue(edge.FromComponent, out result))
edge.Constraint = RelativeConstraintType.Unconstrained;
return result;
}
/// <summary>
/// Locks both edges if they are constrained to the same anchor.
/// </summary>
/// <param name="edge">The edge to check.</param>
/// <param name="otherEdge">The other edge to check.</param>
/// <returns>true if it was able to lock, or false otherwise.</returns>
private static bool LockEdgeAnchor(EdgeStatus edge, EdgeStatus otherEdge) {
bool useDelta = edge.Constraint == RelativeConstraintType.ToAnchor && otherEdge.
Constraint == RelativeConstraintType.ToAnchor && edge.FromAnchor == otherEdge.
FromAnchor;
if (useDelta) {
edge.Constraint = RelativeConstraintType.Locked;
otherEdge.Constraint = RelativeConstraintType.Locked;
edge.Offset = 0.0f;
otherEdge.Offset = 0.0f;
}
return useDelta;
}
/// <summary>
/// Locks an edge if it is constrained to an anchor.
/// </summary>
/// <param name="edge">The edge to check.</param>
private static void LockEdgeAnchor(EdgeStatus edge) {
if (edge.Constraint == RelativeConstraintType.ToAnchor) {
edge.Constraint = RelativeConstraintType.Locked;
edge.Offset = 0.0f;
}
}
/// <summary>
/// Locks an edge if it can be determined from another component.
/// </summary>
/// <param name="edge">The edge to check.</param>
/// <param name="offset">The component's offset in that direction.</param>
/// <param name="otherEdge">The opposing edge of the referenced component.</param>
private static void LockEdgeComponent(EdgeStatus edge, EdgeStatus otherEdge) {
if (edge.Constraint == RelativeConstraintType.ToComponent && otherEdge.Locked) {
edge.Constraint = RelativeConstraintType.Locked;
edge.FromAnchor = otherEdge.FromAnchor;
edge.Offset = otherEdge.Offset;
}
}
/// <summary>
/// Locks an edge if it can be determined from the other edge.
/// </summary>
/// <param name="edge">The edge to check.</param>
/// <param name="size">The component's effective size in that direction.</param>
/// <param name="opposing">The component's other edge.</param>
private static void LockEdgeRelative(EdgeStatus edge, float size, EdgeStatus opposing)
{
if (edge.Constraint == RelativeConstraintType.Unconstrained) {
if (opposing.Locked) {
edge.Constraint = RelativeConstraintType.Locked;
edge.FromAnchor = opposing.FromAnchor;
edge.Offset = opposing.Offset + size;
} else if (opposing.Constraint == RelativeConstraintType.Unconstrained) {
// Both unconstrained, full size
edge.Constraint = RelativeConstraintType.Locked;
edge.FromAnchor = 0.0f;
edge.Offset = 0.0f;
opposing.Constraint = RelativeConstraintType.Locked;
opposing.FromAnchor = 1.0f;
opposing.Offset = 0.0f;
}
}
}
/// <summary>
/// Runs a layout pass in the X direction, resolving edges that can be resolved.
/// </summary>
/// <param name="children">The children to resolve.</param>
/// <returns>true if all children have all X edges constrained, or false otherwise.</returns>
internal static bool RunPassX(this IEnumerable<RelativeLayoutResults> children) {
bool done = true;
foreach (var child in children) {
float width = child.EffectiveWidth;
EdgeStatus l = child.LeftEdge, r = child.RightEdge;
if (LockEdgeAnchor(l, r))
child.UseSizeDeltaX = true;
LockEdgeAnchor(l);
LockEdgeAnchor(r);
LockEdgeRelative(l, -width, r);
LockEdgeRelative(r, width, l);
// Lock to other components
if (child.LeftParams != null)
LockEdgeComponent(l, child.LeftParams.RightEdge);
if (child.RightParams != null)
LockEdgeComponent(r, child.RightParams.LeftEdge);
if (!l.Locked || !r.Locked) done = false;
}
return done;
}
/// <summary>
/// Runs a layout pass in the Y direction, resolving edges that can be resolved.
/// </summary>
/// <param name="children">The children to resolve.</param>
/// <returns>true if all children have all Y edges constrained, or false otherwise.</returns>
internal static bool RunPassY(this IEnumerable<RelativeLayoutResults> children) {
bool done = true;
foreach (var child in children) {
float height = child.EffectiveHeight;
EdgeStatus t = child.TopEdge, b = child.BottomEdge;
if (LockEdgeAnchor(t, b))
child.UseSizeDeltaY = true;
LockEdgeAnchor(b);
LockEdgeAnchor(t);
LockEdgeRelative(b, -height, t);
LockEdgeRelative(t, height, b);
// Lock to other components
if (child.BottomParams != null)
LockEdgeComponent(b, child.BottomParams.TopEdge);
if (child.TopParams != null)
LockEdgeComponent(t, child.TopParams.BottomEdge);
if (!t.Locked || !b.Locked) done = false;
}
return done;
}
/// <summary>
/// Throws an error when resolution fails.
/// </summary>
/// <param name="children">The children, some of which failed to resolve.</param>
/// <param name="limit">The number of passes executed before failing.</param>
/// <param name="direction">The direction that failed.</param>
internal static void ThrowUnresolvable(this IEnumerable<RelativeLayoutResults> children,
int limit, PanelDirection direction) {
var message = new StringBuilder(256);
message.Append("After ").Append(limit);
message.Append(" passes, unable to complete resolution of RelativeLayout:");
foreach (var child in children) {
string name = child.Transform.gameObject.name;
if (direction == PanelDirection.Horizontal) {
if (!child.LeftEdge.Locked) {
message.Append(' ');
message.Append(name);
message.Append(".Left");
}
if (!child.RightEdge.Locked) {
message.Append(' ');
message.Append(name);
message.Append(".Right");
}
} else {
if (!child.BottomEdge.Locked) {
message.Append(' ');
message.Append(name);
message.Append(".Bottom");
}
if (!child.TopEdge.Locked) {
message.Append(' ');
message.Append(name);
message.Append(".Top");
}
}
}
throw new InvalidOperationException(message.ToString());
}
}
}