/* * 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 { /// /// A helper class for RelativeLayout. /// internal static class RelativeLayoutUtil { /// /// Initializes and computes horizontal sizes for the components in this relative /// layout. /// /// The location to store information about these components. /// The components to lay out. /// The constraints defined for these components. internal static void CalcX(this ICollection children, RectTransform all, IDictionary constraints) { var comps = ListPool.Allocate(); var paramMap = DictionaryPool.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(); } /// /// Computes vertical sizes for the components in this relative layout. /// /// The location to store information about these components. internal static void CalcY(this ICollection children) { var comps = ListPool.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(); } /// /// Calculates the minimum size the component must be to support a specific child /// component. /// /// The lower edge constraint. /// The upper edge constraint. /// The component size in that dimension plus margins. /// The minimum parent component size to fit the child. 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; } /// /// Executes the horizontal layout. /// /// The components to lay out. /// The location where components will be temporarily stored. /// The left margin. /// The right margin. internal static void ExecuteX(this IEnumerable children, List 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(); } } /// /// Executes the vertical layout. /// /// The components to lay out. /// The location where components will be temporarily stored. /// The bottom margin. /// The top margin. internal static void ExecuteY(this IEnumerable children, List 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(); } } /// /// Calculates the minimum size in the X direction. /// /// The components to lay out. /// The minimum horizontal size. internal static float GetMinSizeX(this IEnumerable 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; } /// /// Calculates the minimum size in the Y direction. /// /// The components to lay out. /// The minimum vertical size. internal static float GetMinSizeY(this IEnumerable 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; } /// /// Resolves a component reference if needed. /// /// The edge to resolve. /// The location where the component can be looked up. /// The linked parameters for that edge if needed. private static RelativeLayoutResults InitResolve(EdgeStatus edge, IDictionary lookup) { RelativeLayoutResults result = null; if (edge.Constraint == RelativeConstraintType.ToComponent) if (!lookup.TryGetValue(edge.FromComponent, out result)) edge.Constraint = RelativeConstraintType.Unconstrained; return result; } /// /// Locks both edges if they are constrained to the same anchor. /// /// The edge to check. /// The other edge to check. /// true if it was able to lock, or false otherwise. 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; } /// /// Locks an edge if it is constrained to an anchor. /// /// The edge to check. private static void LockEdgeAnchor(EdgeStatus edge) { if (edge.Constraint == RelativeConstraintType.ToAnchor) { edge.Constraint = RelativeConstraintType.Locked; edge.Offset = 0.0f; } } /// /// Locks an edge if it can be determined from another component. /// /// The edge to check. /// The component's offset in that direction. /// The opposing edge of the referenced component. 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; } } /// /// Locks an edge if it can be determined from the other edge. /// /// The edge to check. /// The component's effective size in that direction. /// The component's other edge. 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; } } } /// /// Runs a layout pass in the X direction, resolving edges that can be resolved. /// /// The children to resolve. /// true if all children have all X edges constrained, or false otherwise. internal static bool RunPassX(this IEnumerable 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; } /// /// Runs a layout pass in the Y direction, resolving edges that can be resolved. /// /// The children to resolve. /// true if all children have all Y edges constrained, or false otherwise. internal static bool RunPassY(this IEnumerable 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; } /// /// Throws an error when resolution fails. /// /// The children, some of which failed to resolve. /// The number of passes executed before failing. /// The direction that failed. internal static void ThrowUnresolvable(this IEnumerable 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()); } } }