Initial commit

This commit is contained in:
2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
/*
* 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.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace PeterHan.PLib.UI.Layouts {
/// <summary>
/// The abstract parent of most layout groups.
/// </summary>
public abstract class AbstractLayoutGroup : UIBehaviour, ISettableFlexSize, ILayoutElement
{
/// <summary>
/// Sets an object's layout dirty on the next frame.
/// </summary>
/// <param name="transform">The transform to set dirty.</param>
/// <returns>A coroutine to set it dirty.</returns>
internal static IEnumerator DelayedSetDirty(RectTransform transform) {
yield return null;
LayoutRebuilder.MarkLayoutForRebuild(transform);
}
/// <summary>
/// Removes and destroys any PLib layouts on the component. They will be replaced with
/// a static LayoutElement containing the old size of the component.
/// </summary>
/// <param name="component">The component to cleanse.</param>
internal static void DestroyAndReplaceLayout(GameObject component) {
if (component != null && component.TryGetComponent(out AbstractLayoutGroup
layoutGroup)) {
var replacement = component.AddOrGet<LayoutElement>();
replacement.flexibleHeight = layoutGroup.flexibleHeight;
replacement.flexibleWidth = layoutGroup.flexibleWidth;
replacement.layoutPriority = layoutGroup.layoutPriority;
replacement.minHeight = layoutGroup.minHeight;
replacement.minWidth = layoutGroup.minWidth;
replacement.preferredHeight = layoutGroup.preferredHeight;
replacement.preferredWidth = layoutGroup.preferredWidth;
DestroyImmediate(layoutGroup);
}
}
public float minWidth {
get {
return mMinWidth;
}
set {
mMinWidth = value;
}
}
public float preferredWidth {
get {
return mPreferredWidth;
}
set {
mPreferredWidth = value;
}
}
/// <summary>
/// The flexible width of the completed layout group can be set.
/// </summary>
public float flexibleWidth {
get {
return mFlexibleWidth;
}
set {
mFlexibleWidth = value;
}
}
public float minHeight {
get {
return mMinHeight;
}
set {
mMinHeight = value;
}
}
public float preferredHeight {
get {
return mPreferredHeight;
}
set {
mPreferredHeight = value;
}
}
/// <summary>
/// The flexible height of the completed layout group can be set.
/// </summary>
public float flexibleHeight {
get {
return mFlexibleHeight;
}
set {
mFlexibleHeight = value;
}
}
/// <summary>
/// The priority of this layout group.
/// </summary>
public int layoutPriority {
get {
return mLayoutPriority;
}
set {
mLayoutPriority = value;
}
}
protected RectTransform rectTransform {
get {
if (cachedTransform == null)
cachedTransform = gameObject.rectTransform();
return cachedTransform;
}
}
/// <summary>
/// Whether the layout is currently locked.
/// </summary>
[SerializeField]
protected bool locked;
// The backing fields must be annotated with the attribute to have prefabbed versions
// successfully copy the values
[SerializeField]
private float mMinWidth, mMinHeight, mPreferredWidth, mPreferredHeight;
[SerializeField]
private float mFlexibleWidth, mFlexibleHeight;
[SerializeField]
private int mLayoutPriority;
/// <summary>
/// The cached rect transform to speed up layout.
/// </summary>
private RectTransform cachedTransform;
protected AbstractLayoutGroup() {
cachedTransform = null;
locked = false;
mLayoutPriority = 1;
}
public abstract void CalculateLayoutInputHorizontal();
public abstract void CalculateLayoutInputVertical();
/// <summary>
/// Triggers a layout with the current parent, and then locks the layout size. Further
/// attempts to automatically lay out the component, unless UnlockLayout is called,
/// will not trigger any action.
///
/// The resulting layout has very good performance, but cannot adapt to changes in the
/// size of its children or its own size.
/// </summary>
/// <returns>The computed size of this component when locked.</returns>
public virtual Vector2 LockLayout() {
var rt = gameObject.rectTransform();
if (rt != null) {
locked = false;
CalculateLayoutInputHorizontal();
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, minWidth);
SetLayoutHorizontal();
CalculateLayoutInputVertical();
rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, minHeight);
SetLayoutVertical();
locked = true;
}
return new Vector2(minWidth, minHeight);
}
protected override void OnDidApplyAnimationProperties() {
base.OnDidApplyAnimationProperties();
SetDirty();
}
protected override void OnDisable() {
base.OnDisable();
SetDirty();
}
protected override void OnEnable() {
base.OnEnable();
SetDirty();
}
protected override void OnRectTransformDimensionsChange() {
base.OnRectTransformDimensionsChange();
SetDirty();
}
/// <summary>
/// Sets this layout as dirty.
/// </summary>
protected virtual void SetDirty() {
if (gameObject != null && IsActive()) {
if (CanvasUpdateRegistry.IsRebuildingLayout())
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
else
StartCoroutine(DelayedSetDirty(rectTransform));
}
}
public abstract void SetLayoutHorizontal();
public abstract void SetLayoutVertical();
/// <summary>
/// Unlocks the layout, allowing it to again dynamically resize when component sizes
/// are changed.
/// </summary>
public virtual void UnlockLayout() {
locked = false;
LayoutRebuilder.MarkLayoutForRebuild(gameObject.rectTransform());
}
}
}

View File

@@ -0,0 +1,266 @@
/*
* 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.
*/
//#define DEBUG_LAYOUT
using PeterHan.PLib.UI.Layouts;
using System;
using UnityEngine;
using UnityEngine.UI;
namespace PeterHan.PLib.UI {
/// <summary>
/// A freezable, flexible layout manager that fixes the issues I am having with
/// HorizontalLayoutGroup and VerticalLayoutGroup. You get a content size fitter for
/// free too!
///
/// Intended to work something like Java's BoxLayout...
/// </summary>
public sealed class BoxLayoutGroup : AbstractLayoutGroup {
/// <summary>
/// Calculates the size of the box layout container.
/// </summary>
/// <param name="obj">The container to lay out.</param>
/// <param name="args">The parameters to use for layout.</param>
/// <param name="direction">The direction which is being calculated.</param>
/// <returns>The minimum and preferred box layout size.</returns>
private static BoxLayoutResults Calc(GameObject obj, BoxLayoutParams args,
PanelDirection direction) {
var transform = obj.AddOrGet<RectTransform>();
int n = transform.childCount;
var result = new BoxLayoutResults(direction, n);
var components = ListPool<Component, BoxLayoutGroup>.Allocate();
for (int i = 0; i < n; i++) {
var child = transform.GetChild(i)?.gameObject;
if (child != null && child.activeInHierarchy) {
// Only on active game objects
components.Clear();
child.GetComponents(components);
var hc = PUIUtils.CalcSizes(child, direction, components);
if (!hc.ignore) {
if (args.Direction == direction)
result.Accum(hc, args.Spacing);
else
result.Expand(hc);
result.children.Add(hc);
}
}
}
components.Recycle();
return result;
}
/// <summary>
/// Lays out components in the box layout container.
/// </summary>
/// <param name="args">The parameters to use for layout.</param>
/// <param name="required">The calculated minimum and preferred sizes.</param>
/// <param name="size">The total available size in this dimension.</param>
private static void DoLayout(BoxLayoutParams args, BoxLayoutResults required,
float size) {
if (required == null)
throw new ArgumentNullException(nameof(required));
var direction = required.direction;
var status = new BoxLayoutStatus(direction, args.Margin ?? new RectOffset(), size);
if (args.Direction == direction)
DoLayoutLinear(required, args, status);
else
DoLayoutPerp(required, args, status);
}
/// <summary>
/// Lays out components in the box layout container parallel to the layout axis.
/// </summary>
/// <param name="required">The calculated minimum and preferred sizes.</param>
/// <param name="args">The parameters to use for layout.</param>
/// <param name="status">The current status of layout.</param>
private static void DoLayoutLinear(BoxLayoutResults required, BoxLayoutParams args,
BoxLayoutStatus status) {
var total = required.total;
var components = ListPool<ILayoutController, BoxLayoutGroup>.Allocate();
var direction = args.Direction;
// Determine flex size ratio
float size = status.size, prefRatio = 0.0f, minSize = total.min, prefSize =
total.preferred, excess = Math.Max(0.0f, size - prefSize), flexTotal = total.
flexible, offset = status.offset, spacing = args.Spacing;
if (size > minSize && prefSize > minSize)
// Do not divide by 0
prefRatio = Math.Min(1.0f, (size - minSize) / (prefSize - minSize));
if (excess > 0.0f && flexTotal == 0.0f)
// If no components can be expanded, offset all
offset += PUIUtils.GetOffset(args.Alignment, status.direction, excess);
foreach (var child in required.children) {
var obj = child.source;
// Active objects only
if (obj != null && obj.activeInHierarchy) {
float compSize = child.min;
if (prefRatio > 0.0f)
compSize += (child.preferred - child.min) * prefRatio;
if (excess > 0.0f && flexTotal > 0.0f)
compSize += excess * child.flexible / flexTotal;
// Place and size component
obj.AddOrGet<RectTransform>().SetInsetAndSizeFromParentEdge(status.edge,
offset, compSize);
offset += compSize + ((compSize > 0.0f) ? spacing : 0.0f);
// Invoke SetLayout on dependents
components.Clear();
obj.GetComponents(components);
foreach (var component in components)
if (direction == PanelDirection.Horizontal)
component.SetLayoutHorizontal();
else // if (direction == PanelDirection.Vertical)
component.SetLayoutVertical();
}
}
components.Recycle();
}
/// <summary>
/// Lays out components in the box layout container against the layout axis.
/// </summary>
/// <param name="required">The calculated minimum and preferred sizes.</param>
/// <param name="args">The parameters to use for layout.</param>
/// <param name="status">The current status of layout.</param>
private static void DoLayoutPerp(BoxLayoutResults required, BoxLayoutParams args,
BoxLayoutStatus status) {
var components = ListPool<ILayoutController, BoxLayoutGroup>.Allocate();
var direction = args.Direction;
float size = status.size;
foreach (var child in required.children) {
var obj = child.source;
// Active objects only
if (obj != null && obj.activeInHierarchy) {
float compSize = size;
if (child.flexible <= 0.0f)
// Does not expand to all
compSize = Math.Min(compSize, child.preferred);
float offset = (size > compSize) ? PUIUtils.GetOffset(args.Alignment,
status.direction, size - compSize) : 0.0f;
// Place and size component
obj.AddOrGet<RectTransform>().SetInsetAndSizeFromParentEdge(status.edge,
offset + status.offset, compSize);
// Invoke SetLayout on dependents
components.Clear();
obj.GetComponents(components);
foreach (var component in components)
if (direction == PanelDirection.Horizontal)
component.SetLayoutVertical();
else // if (direction == PanelDirection.Vertical)
component.SetLayoutHorizontal();
}
}
components.Recycle();
}
/// <summary>
/// The parameters used to set up this box layout.
/// </summary>
public BoxLayoutParams Params {
get {
return parameters;
}
set {
parameters = value ?? throw new ArgumentNullException(nameof(Params));
}
}
/// <summary>
/// Results from the horizontal calculation pass.
/// </summary>
private BoxLayoutResults horizontal;
/// <summary>
/// The parameters used to set up this box layout.
/// </summary>
[SerializeField]
private BoxLayoutParams parameters;
/// <summary>
/// Results from the vertical calculation pass.
/// </summary>
private BoxLayoutResults vertical;
internal BoxLayoutGroup() {
horizontal = null;
layoutPriority = 1;
parameters = new BoxLayoutParams();
vertical = null;
}
public override void CalculateLayoutInputHorizontal() {
if (!locked) {
var margin = parameters.Margin;
float gap = (margin == null) ? 0.0f : margin.left + margin.right;
horizontal = Calc(gameObject, parameters, PanelDirection.Horizontal);
var hTotal = horizontal.total;
minWidth = hTotal.min + gap;
preferredWidth = hTotal.preferred + gap;
#if DEBUG_LAYOUT
PUIUtils.LogUIDebug("CalculateLayoutInputHorizontal for {0} preferred {1:F2}".
F(gameObject.name, preferredWidth));
#endif
}
}
public override void CalculateLayoutInputVertical() {
if (!locked) {
var margin = parameters.Margin;
float gap = (margin == null) ? 0.0f : margin.top + margin.bottom;
vertical = Calc(gameObject, parameters, PanelDirection.Vertical);
var vTotal = vertical.total;
minHeight = vTotal.min + gap;
preferredHeight = vTotal.preferred + gap;
#if DEBUG_LAYOUT
PUIUtils.LogUIDebug("CalculateLayoutInputVertical for {0} preferred {1:F2}".F(
gameObject.name, preferredHeight));
#endif
}
}
protected override void OnDisable() {
base.OnDisable();
horizontal = null;
vertical = null;
}
protected override void OnEnable() {
base.OnEnable();
horizontal = null;
vertical = null;
}
public override void SetLayoutHorizontal() {
if (horizontal != null && !locked) {
#if DEBUG_LAYOUT
PUIUtils.LogUIDebug("SetLayoutHorizontal for {0} resolved width to {1:F2}".F(
gameObject.name, rectTransform.rect.width));
#endif
DoLayout(parameters, horizontal, rectTransform.rect.width);
}
}
public override void SetLayoutVertical() {
if (vertical != null && !locked) {
#if DEBUG_LAYOUT
PUIUtils.LogUIDebug("SetLayoutVertical for {0} resolved height to {1:F2}".F(
gameObject.name, rectTransform.rect.height));
#endif
DoLayout(parameters, vertical, rectTransform.rect.height);
}
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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 UnityEngine;
namespace PeterHan.PLib.UI {
/// <summary>
/// The parameters used for laying out a box layout.
/// </summary>
[Serializable]
public sealed class BoxLayoutParams {
/// <summary>
/// The alignment to use for components that are not big enough to fit and have no
/// flexible width.
/// </summary>
public TextAnchor Alignment { get; set; }
/// <summary>
/// The direction of layout.
/// </summary>
public PanelDirection Direction { get; set; }
/// <summary>
/// The margin between the children and the component edge.
/// </summary>
public RectOffset Margin { get; set; }
/// <summary>
/// The spacing between components.
/// </summary>
public float Spacing { get; set; }
public BoxLayoutParams() {
Alignment = TextAnchor.MiddleCenter;
Direction = PanelDirection.Horizontal;
Margin = null;
Spacing = 0.0f;
}
public override string ToString() {
return string.Format("BoxLayoutParams[Alignment={0},Direction={1},Spacing={2:F2}]",
Alignment, Direction, Spacing);
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* 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 {
/// <summary>
/// A class which stores the results of a single box layout calculation pass.
/// </summary>
internal sealed class BoxLayoutResults {
/// <summary>
/// The components which were laid out.
/// </summary>
public readonly ICollection<LayoutSizes> children;
/// <summary>
/// The current direction of flow.
/// </summary>
public readonly PanelDirection direction;
/// <summary>
/// Whether any spaces have been added yet for minimum size.
/// </summary>
private bool haveMinSpace;
/// <summary>
/// Whether any spaces have been added yet for preferred size.
/// </summary>
private bool havePrefSpace;
/// <summary>
/// The total sizes.
/// </summary>
public LayoutSizes total;
internal BoxLayoutResults(PanelDirection direction, int presize) {
children = new List<LayoutSizes>(presize);
this.direction = direction;
haveMinSpace = false;
havePrefSpace = false;
total = new LayoutSizes();
}
/// <summary>
/// Accumulates another component into the results.
/// </summary>
/// <param name="sizes">The size of the component to add.</param>
/// <param name="spacing">The component spacing.</param>
public void Accum(LayoutSizes sizes, float spacing) {
float newMin = sizes.min, newPreferred = sizes.preferred;
if (newMin > 0.0f) {
// Skip one space
if (haveMinSpace)
newMin += spacing;
haveMinSpace = true;
}
total.min += newMin;
if (newPreferred > 0.0f) {
// Skip one space
if (havePrefSpace)
newPreferred += spacing;
havePrefSpace = true;
}
total.preferred += newPreferred;
total.flexible += sizes.flexible;
}
/// <summary>
/// Expands the results around another component.
/// </summary>
/// <param name="sizes">The size of the component to expand to.</param>
public void Expand(LayoutSizes sizes) {
float newMin = sizes.min, newPreferred = sizes.preferred, newFlexible = sizes.
flexible;
if (newMin > total.min)
total.min = newMin;
if (newPreferred > total.preferred)
total.preferred = newPreferred;
if (newFlexible > total.flexible)
total.flexible = newFlexible;
}
public override string ToString() {
return direction + " " + total;
}
}
/// <summary>
/// Maintains the status of a layout in progress.
/// </summary>
internal sealed class BoxLayoutStatus {
/// <summary>
/// The current direction of flow.
/// </summary>
public readonly PanelDirection direction;
/// <summary>
/// The edge from where layout started.
/// </summary>
public readonly RectTransform.Edge edge;
/// <summary>
/// The next component's offset.
/// </summary>
public readonly float offset;
/// <summary>
/// The component size in that direction minus margins.
/// </summary>
public readonly float size;
internal BoxLayoutStatus(PanelDirection direction, RectOffset margins, float size) {
this.direction = direction;
switch (direction) {
case PanelDirection.Horizontal:
edge = RectTransform.Edge.Left;
offset = margins.left;
this.size = size - offset - margins.right;
break;
case PanelDirection.Vertical:
edge = RectTransform.Edge.Top;
offset = margins.top;
this.size = size - offset - margins.bottom;
break;
default:
throw new ArgumentException("direction");
}
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* 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.UI.Layouts;
using System;
using UnityEngine;
using UnityEngine.UI;
namespace PeterHan.PLib.UI {
/// <summary>
/// A freezable layout manager that displays one of its contained objects at a time.
/// Unlike other layout groups, even inactive children are considered for sizing.
/// </summary>
public sealed class CardLayoutGroup : AbstractLayoutGroup {
/// <summary>
/// Calculates the size of the card layout container.
/// </summary>
/// <param name="obj">The container to lay out.</param>
/// <param name="args">The parameters to use for layout.</param>
/// <param name="direction">The direction which is being calculated.</param>
/// <returns>The minimum and preferred box layout size.</returns>
private static CardLayoutResults Calc(GameObject obj, PanelDirection direction) {
var transform = obj.AddOrGet<RectTransform>();
int n = transform.childCount;
var result = new CardLayoutResults(direction, n);
var components = ListPool<Component, BoxLayoutGroup>.Allocate();
for (int i = 0; i < n; i++) {
var child = transform.GetChild(i)?.gameObject;
if (child != null) {
bool active = child.activeInHierarchy;
// Not only on active game objects
components.Clear();
child.GetComponents(components);
child.SetActive(true);
var hc = PUIUtils.CalcSizes(child, direction, components);
if (!hc.ignore) {
result.Expand(hc);
result.children.Add(hc);
}
child.SetActive(active);
}
}
components.Recycle();
return result;
}
/// <summary>
/// Lays out components in the card layout container.
/// </summary>
/// <param name="margin">The margin to allow around the components.</param>
/// <param name="required">The calculated minimum and preferred sizes.</param>
/// <param name="size">The total available size in this dimension.</param>
private static void DoLayout(RectOffset margin, CardLayoutResults required, float size)
{
if (required == null)
throw new ArgumentNullException(nameof(required));
var direction = required.direction;
var components = ListPool<ILayoutController, BoxLayoutGroup>.Allocate();
// Compensate for margins
if (direction == PanelDirection.Horizontal)
size -= margin.left + margin.right;
else
size -= margin.top + margin.bottom;
foreach (var child in required.children) {
var obj = child.source;
if (obj != null) {
float compSize = PUIUtils.GetProperSize(child, size);
// Place and size component
var transform = obj.AddOrGet<RectTransform>();
if (direction == PanelDirection.Horizontal)
transform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left,
margin.left, compSize);
else
transform.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top,
margin.top, compSize);
// Invoke SetLayout on dependents
components.Clear();
obj.GetComponents(components);
foreach (var component in components)
if (direction == PanelDirection.Horizontal)
component.SetLayoutHorizontal();
else // if (direction == PanelDirection.Vertical)
component.SetLayoutVertical();
}
}
components.Recycle();
}
/// <summary>
/// The margin around the components as a whole.
/// </summary>
public RectOffset Margin {
get {
return margin;
}
set {
margin = value;
}
}
/// <summary>
/// Results from the horizontal calculation pass.
/// </summary>
private CardLayoutResults horizontal;
/// <summary>
/// The margin around the components as a whole.
/// </summary>
[SerializeField]
private RectOffset margin;
/// <summary>
/// Results from the vertical calculation pass.
/// </summary>
private CardLayoutResults vertical;
internal CardLayoutGroup() {
horizontal = null;
layoutPriority = 1;
vertical = null;
}
public override void CalculateLayoutInputHorizontal() {
if (!locked) {
var margin = Margin;
float gap = (margin == null) ? 0.0f : margin.left + margin.right;
horizontal = Calc(gameObject, PanelDirection.Horizontal);
var hTotal = horizontal.total;
minWidth = hTotal.min + gap;
preferredWidth = hTotal.preferred + gap;
}
}
public override void CalculateLayoutInputVertical() {
if (!locked) {
var margin = Margin;
float gap = (margin == null) ? 0.0f : margin.top + margin.bottom;
vertical = Calc(gameObject, PanelDirection.Vertical);
var vTotal = vertical.total;
minHeight = vTotal.min + gap;
preferredHeight = vTotal.preferred + gap;
}
}
protected override void OnDisable() {
base.OnDisable();
horizontal = null;
vertical = null;
}
protected override void OnEnable() {
base.OnEnable();
horizontal = null;
vertical = null;
}
/// <summary>
/// Switches the active card.
/// </summary>
/// <param name="card">The child to make active, or null to inactivate all children.</param>
public void SetActiveCard(GameObject card) {
int n = transform.childCount;
for (int i = 0; i < n; i++) {
var child = transform.GetChild(i)?.gameObject;
if (child != null)
child.SetActive(child == card);
}
}
/// <summary>
/// Switches the active card.
/// </summary>
/// <param name="index">The child index to make active, or -1 to inactivate all children.</param>
public void SetActiveCard(int index) {
int n = transform.childCount;
for (int i = 0; i < n; i++) {
var child = transform.GetChild(i)?.gameObject;
if (child != null)
child.SetActive(i == index);
}
}
public override void SetLayoutHorizontal() {
if (horizontal != null && !locked)
DoLayout(Margin ?? new RectOffset(), horizontal, rectTransform.rect.width);
}
public override void SetLayoutVertical() {
if (vertical != null && !locked)
DoLayout(Margin ?? new RectOffset(), vertical, rectTransform.rect.height);
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.Collections.Generic;
namespace PeterHan.PLib.UI.Layouts {
/// <summary>
/// A class which stores the results of a single card layout calculation pass.
/// </summary>
internal sealed class CardLayoutResults {
/// <summary>
/// The components which were laid out.
/// </summary>
public readonly ICollection<LayoutSizes> children;
/// <summary>
/// The current direction of flow.
/// </summary>
public readonly PanelDirection direction;
/// <summary>
/// The total sizes.
/// </summary>
public LayoutSizes total;
internal CardLayoutResults(PanelDirection direction, int presize) {
children = new List<LayoutSizes>(presize);
this.direction = direction;
total = new LayoutSizes();
}
/// <summary>
/// Expands the results around another component.
/// </summary>
/// <param name="sizes">The size of the component to expand to.</param>
public void Expand(LayoutSizes sizes) {
float newMin = sizes.min, newPreferred = sizes.preferred, newFlexible =
sizes.flexible;
if (newMin > total.min)
total.min = newMin;
if (newPreferred > total.preferred)
total.preferred = newPreferred;
if (newFlexible > total.flexible)
total.flexible = newFlexible;
}
public override string ToString() {
return direction + " " + total;
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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;
namespace PeterHan.PLib.UI {
/// <summary>
/// A component in the grid with its placement information.
/// </summary>
[Serializable]
internal sealed class GridComponent<T> : GridComponentSpec where T : class {
/// <summary>
/// The object to place here.
/// </summary>
public T Item { get; }
internal GridComponent(GridComponentSpec spec, T item) {
Alignment = spec.Alignment;
Item = item;
Column = spec.Column;
ColumnSpan = spec.ColumnSpan;
Margin = spec.Margin;
Row = spec.Row;
RowSpan = spec.RowSpan;
}
}
}

View File

@@ -0,0 +1,166 @@
/*
* 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 System;
using UnityEngine;
namespace PeterHan.PLib.UI {
/// <summary>
/// Stores the state of a component in a grid layout.
/// </summary>
public class GridComponentSpec {
/// <summary>
/// The alignment of the component.
/// </summary>
public TextAnchor Alignment { get; set; }
/// <summary>
/// The column of the component.
/// </summary>
public int Column { get; set; }
/// <summary>
/// The number of columns this component spans.
/// </summary>
public int ColumnSpan { get; set; }
/// <summary>
/// The margin to allocate around each component.
/// </summary>
public RectOffset Margin { get; set; }
/// <summary>
/// The row of the component.
/// </summary>
public int Row { get; set; }
/// <summary>
/// The number of rows this component spans.
/// </summary>
public int RowSpan { get; set; }
internal GridComponentSpec() { }
/// <summary>
/// Creates a new grid component specification. While the row and column are mandatory,
/// the other attributes can be optionally specified in the initializer.
/// </summary>
/// <param name="row">The row to place the component.</param>
/// <param name="column">The column to place the component.</param>
public GridComponentSpec(int row, int column) {
if (row < 0)
throw new ArgumentOutOfRangeException(nameof(row));
if (column < 0)
throw new ArgumentOutOfRangeException(nameof(column));
Alignment = TextAnchor.MiddleCenter;
Row = row;
Column = column;
Margin = null;
RowSpan = 1;
ColumnSpan = 1;
}
public override string ToString() {
return string.Format("GridComponentSpec[Row={0:D},Column={1:D},RowSpan={2:D},ColumnSpan={3:D}]",
Row, Column, RowSpan, ColumnSpan);
}
}
/// <summary>
/// The specifications for one column in a grid layout.
/// </summary>
[Serializable]
public sealed class GridColumnSpec {
/// <summary>
/// The flexible width of this grid column. If there is space left after all
/// columns get their nominal width, each column will get a fraction of the space
/// left proportional to their FlexWidth value as a ratio to the total flexible
/// width values.
/// </summary>
public float FlexWidth { get; }
/// <summary>
/// The nominal width of this grid column. If zero, the preferred width of the
/// largest component is used. If there are no components in this column (possibly
/// because the only components in this row all have column spans from other
/// columns), the width will be zero!
/// </summary>
public float Width { get; }
/// <summary>
/// Creates a new grid column specification.
/// </summary>
/// <param name="width">The column's base width, or 0 to auto-size the column to the
/// preferred width of its largest component.</param>
/// <param name="flex">The percentage of the leftover width the column should occupy.</param>
public GridColumnSpec(float width = 0.0f, float flex = 0.0f) {
if (width.IsNaNOrInfinity() || width < 0.0f)
throw new ArgumentOutOfRangeException(nameof(width));
if (flex.IsNaNOrInfinity() || flex < 0.0f)
throw new ArgumentOutOfRangeException(nameof(flex));
Width = width;
FlexWidth = flex;
}
public override string ToString() {
return string.Format("GridColumnSpec[Width={0:F2}]", Width);
}
}
/// <summary>
/// The specifications for one row in a grid layout.
/// </summary>
[Serializable]
public sealed class GridRowSpec {
/// <summary>
/// The flexible height of this grid row. If there is space left after all rows
/// get their nominal height, each row will get a fraction of the space left
/// proportional to their FlexHeight value as a ratio to the total flexible
/// height values.
/// </summary>
public float FlexHeight { get; }
/// <summary>
/// The nominal height of this grid row. If zero, the preferred height of the
/// largest component is used. If there are no components in this row (possibly
/// because the only components in this row all have row spans from other rows),
/// the height will be zero!
/// </summary>
public float Height { get; }
/// <summary>
/// Creates a new grid row specification.
/// </summary>
/// <param name="height">The row's base width, or 0 to auto-size the row to the
/// preferred height of its largest component.</param>
/// <param name="flex">The percentage of the leftover height the row should occupy.</param>
public GridRowSpec(float height = 0.0f, float flex = 0.0f) {
if (height.IsNaNOrInfinity() || height < 0.0f)
throw new ArgumentOutOfRangeException(nameof(height));
if (flex.IsNaNOrInfinity() || flex < 0.0f)
throw new ArgumentOutOfRangeException(nameof(flex));
Height = height;
FlexHeight = flex;
}
public override string ToString() {
return string.Format("GridRowSpec[Height={0:F2}]", Height);
}
}
}

View File

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

View File

@@ -0,0 +1,349 @@
/*
* 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();
}
}
}
}

View File

@@ -0,0 +1,292 @@
/*
* 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.UI.Layouts;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace PeterHan.PLib.UI {
/// <summary>
/// A layout group based on the constraints defined in RelativeLayout. Allows the same
/// fast relative positioning that RelativeLayout does, but can respond to changes in the
/// size of its containing components.
/// </summary>
public sealed class RelativeLayoutGroup : AbstractLayoutGroup,
ISerializationCallbackReceiver {
/// <summary>
/// The margin added around all components in the layout. This is in addition to any
/// margins around the components.
///
/// Note that this margin is not taken into account with percentage based anchors.
/// Items anchored to the extremes will always work fine. Items anchored in the middle
/// will use the middle <b>before</b> margins are effective.
/// </summary>
public RectOffset Margin {
get {
return margin;
}
set {
margin = value;
}
}
/// <summary>
/// Constraints for each object are stored here.
/// </summary>
private readonly IDictionary<GameObject, RelativeLayoutParams> locConstraints;
/// <summary>
/// The serialized constraints.
/// </summary>
[SerializeField]
private IList<KeyValuePair<GameObject, RelativeLayoutParams>> serialConstraints;
/// <summary>
/// The margin around the components as a whole.
/// </summary>
[SerializeField]
private RectOffset margin;
/// <summary>
/// The results of the layout in progress.
/// </summary>
private readonly IList<RelativeLayoutResults> results;
internal RelativeLayoutGroup() {
layoutPriority = 1;
locConstraints = new Dictionary<GameObject, RelativeLayoutParams>(32);
results = new List<RelativeLayoutResults>(32);
serialConstraints = null;
}
/// <summary>
/// Retrieves the parameters for a child game object. Creates an entry if none exists
/// for this component.
/// </summary>
/// <param name="item">The item to look up.</param>
/// <returns>The parameters for that object.</returns>
private RelativeLayoutParams AddOrGet(GameObject item) {
if (!locConstraints.TryGetValue(item, out RelativeLayoutParams param))
locConstraints[item] = param = new RelativeLayoutParams();
return param;
}
public RelativeLayoutGroup AnchorXAxis(GameObject item, float anchor = 0.5f) {
SetLeftEdge(item, fraction: anchor);
return SetRightEdge(item, fraction: anchor);
}
public RelativeLayoutGroup AnchorYAxis(GameObject item, float anchor = 0.5f) {
SetTopEdge(item, fraction: anchor);
return SetBottomEdge(item, fraction: anchor);
}
public override void CalculateLayoutInputHorizontal() {
var all = gameObject?.rectTransform();
if (all != null && !locked) {
int ml, mr, passes, limit;
if (Margin == null)
ml = mr = 0;
else {
ml = Margin.left;
mr = Margin.right;
}
// X layout
results.CalcX(all, locConstraints);
if (results.Count > 0) {
limit = 2 * results.Count;
for (passes = 0; passes < limit && !results.RunPassX(); passes++) ;
if (passes >= limit)
results.ThrowUnresolvable(passes, PanelDirection.Horizontal);
}
minWidth = preferredWidth = results.GetMinSizeX() + ml + mr;
}
}
public override void CalculateLayoutInputVertical() {
var all = gameObject?.rectTransform();
if (all != null && !locked) {
int passes, limit = 2 * results.Count, mt, mb;
if (Margin == null)
mt = mb = 0;
else {
mt = Margin.top;
mb = Margin.bottom;
}
// Y layout
if (results.Count > 0) {
results.CalcY();
for (passes = 0; passes < limit && !results.RunPassY(); passes++) ;
if (passes >= limit)
results.ThrowUnresolvable(passes, PanelDirection.Vertical);
}
minHeight = preferredHeight = results.GetMinSizeY() + mt + mb;
}
}
/// <summary>
/// Imports the data from RelativeLayout for compatibility.
/// </summary>
/// <param name="values">The raw data to import.</param>
internal void Import(IDictionary<GameObject, RelativeLayoutParams> values) {
locConstraints.Clear();
foreach (var pair in values)
locConstraints[pair.Key] = pair.Value;
}
public void OnBeforeSerialize() {
int n = locConstraints.Count;
if (n > 0) {
serialConstraints = new List<KeyValuePair<GameObject, RelativeLayoutParams>>(n);
foreach (var pair in locConstraints)
serialConstraints.Add(pair);
}
}
public void OnAfterDeserialize() {
if (serialConstraints != null) {
locConstraints.Clear();
foreach (var pair in serialConstraints)
locConstraints[pair.Key] = pair.Value;
serialConstraints = null;
}
}
public RelativeLayoutGroup OverrideSize(GameObject item, Vector2 size) {
if (item != null)
AddOrGet(item).OverrideSize = size;
return this;
}
public RelativeLayoutGroup SetBottomEdge(GameObject item, float fraction = -1.0f,
GameObject above = null) {
if (item != null) {
if (above == item)
throw new ArgumentException("Component cannot refer directly to itself");
SetEdge(AddOrGet(item).BottomEdge, fraction, above);
}
return this;
}
protected override void SetDirty() {
if (!locked)
base.SetDirty();
}
/// <summary>
/// Sets a component's edge constraint.
/// </summary>
/// <param name="edge">The edge to set.</param>
/// <param name="fraction">The fraction of the parent to anchor.</param>
/// <param name="child">The other component to anchor.</param>
private void SetEdge(RelativeLayoutParams.EdgeStatus edge, float fraction,
GameObject child) {
if (fraction >= 0.0f && fraction <= 1.0f) {
edge.Constraint = RelativeConstraintType.ToAnchor;
edge.FromAnchor = fraction;
edge.FromComponent = null;
} else if (child != null) {
edge.Constraint = RelativeConstraintType.ToComponent;
edge.FromComponent = child;
} else {
edge.Constraint = RelativeConstraintType.Unconstrained;
edge.FromComponent = null;
}
}
public override void SetLayoutHorizontal() {
if (!locked && results.Count > 0) {
var components = ListPool<ILayoutController, RelativeLayoutGroup>.Allocate();
int ml, mr;
if (Margin == null)
ml = mr = 0;
else {
ml = Margin.left;
mr = Margin.right;
}
// Lay out children
results.ExecuteX(components, ml, mr);
components.Recycle();
}
}
public override void SetLayoutVertical() {
if (!locked && results.Count > 0) {
var components = ListPool<ILayoutController, RelativeLayoutGroup>.Allocate();
int mt, mb;
if (Margin == null)
mt = mb = 0;
else {
mt = Margin.top;
mb = Margin.bottom;
}
// Lay out children
results.ExecuteY(components, mt, mb);
components.Recycle();
}
}
public RelativeLayoutGroup SetLeftEdge(GameObject item, float fraction = -1.0f,
GameObject toRight = null) {
if (item != null) {
if (toRight == item)
throw new ArgumentException("Component cannot refer directly to itself");
SetEdge(AddOrGet(item).LeftEdge, fraction, toRight);
}
return this;
}
public RelativeLayoutGroup SetMargin(GameObject item, RectOffset insets) {
if (item != null)
AddOrGet(item).Insets = insets;
return this;
}
/// <summary>
/// Sets all layout parameters of an object at once.
/// </summary>
/// <param name="item">The item to configure.</param>
/// <param name="rawParams">The raw parameters to use.</param>
internal void SetRaw(GameObject item, RelativeLayoutParams rawParams) {
if (item != null && rawParams != null)
locConstraints[item] = rawParams;
}
public RelativeLayoutGroup SetRightEdge(GameObject item, float fraction = -1.0f,
GameObject toLeft = null) {
if (item != null) {
if (toLeft == item)
throw new ArgumentException("Component cannot refer directly to itself");
SetEdge(AddOrGet(item).RightEdge, fraction, toLeft);
}
return this;
}
public RelativeLayoutGroup SetTopEdge(GameObject item, float fraction = -1.0f,
GameObject below = null) {
if (item != null) {
if (below == item)
throw new ArgumentException("Component cannot refer directly to itself");
SetEdge(AddOrGet(item).TopEdge, fraction, below);
}
return this;
}
}
}

View File

@@ -0,0 +1,185 @@
/*
* 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 UnityEngine;
namespace PeterHan.PLib.UI.Layouts {
/// <summary>
/// Stores constraints applied to a game object in a relative layout.
/// </summary>
[Serializable]
internal sealed class RelativeLayoutParams : RelativeLayoutParamsBase<GameObject> { }
/// <summary>
/// Stores constraints applied to an object in a relative layout.
/// </summary>
/// <typeparam name="T">The type of the target object.</typeparam>
internal class RelativeLayoutParamsBase<T> {
/// <summary>
/// The anchored position of the bottom edge.
/// </summary>
internal EdgeStatus BottomEdge { get; }
/// <summary>
/// The insets. If null, insets are all zero.
/// </summary>
internal RectOffset Insets { get; set; }
/// <summary>
/// The anchored position of the left edge.
/// </summary>
internal EdgeStatus LeftEdge { get; }
/// <summary>
/// Overrides the size of the component if set.
/// </summary>
internal Vector2 OverrideSize { get; set; }
/// <summary>
/// The anchored position of the right edge.
/// </summary>
internal EdgeStatus RightEdge { get; }
/// <summary>
/// The anchored position of the top edge.
/// </summary>
internal EdgeStatus TopEdge { get; }
internal RelativeLayoutParamsBase() {
Insets = null;
BottomEdge = new EdgeStatus();
LeftEdge = new EdgeStatus();
RightEdge = new EdgeStatus();
TopEdge = new EdgeStatus();
OverrideSize = Vector2.zero;
}
/// <summary>
/// The edge position determined for a component.
/// </summary>
[Serializable]
internal sealed class EdgeStatus {
/// <summary>
/// The type of constraint to use for this relative layout.
/// </summary>
internal RelativeConstraintType Constraint;
/// <summary>
/// The anchor position in the component that sets the relative anchor.
///
/// 0.0f is the bottom/left, 1.0f is the top/right.
/// </summary>
internal float FromAnchor;
/// <summary>
/// The component to which this edge is anchored.
/// </summary>
internal T FromComponent;
/// <summary>
/// The offset in pixels from the anchor. + is upwards/rightwards, - is downwards/
/// leftwards.
/// </summary>
internal float Offset;
/// <summary>
/// True if the position has been locked down in the code.
/// Locked should only be set by the layout manager, crashes may occur otherwise.
/// </summary>
public bool Locked {
get {
return Constraint == RelativeConstraintType.Locked;
}
}
/// <summary>
/// True if the position is not constrained to anything.
/// </summary>
public bool Unconstrained {
get {
return Constraint == RelativeConstraintType.Unconstrained;
}
}
internal EdgeStatus() {
FromAnchor = 0.0f;
FromComponent = default;
Offset = 0.0f;
Constraint = RelativeConstraintType.Unconstrained;
}
/// <summary>
/// Copies data from another edge status object.
/// </summary>
/// <param name="other">The object to copy.</param>
internal void CopyFrom(EdgeStatus other) {
if (other == null)
throw new ArgumentNullException(nameof(other));
switch (Constraint = other.Constraint) {
case RelativeConstraintType.ToComponent:
FromComponent = other.FromComponent;
break;
case RelativeConstraintType.ToAnchor:
FromAnchor = other.FromAnchor;
break;
case RelativeConstraintType.Locked:
FromAnchor = other.FromAnchor;
Offset = other.Offset;
break;
case RelativeConstraintType.Unconstrained:
default:
break;
}
}
public override bool Equals(object obj) {
// Caution: floats are imprecise
return obj is EdgeStatus other && other.FromAnchor == FromAnchor && other.
Offset == Offset && Constraint == other.Constraint;
}
public override int GetHashCode() {
return 37 * (37 * FromAnchor.GetHashCode() + Offset.GetHashCode()) +
Constraint.GetHashCode();
}
/// <summary>
/// Resets these offsets to unlocked.
/// </summary>
public void Reset() {
Constraint = RelativeConstraintType.Unconstrained;
Offset = 0.0f;
FromAnchor = 0.0f;
FromComponent = default;
}
public override string ToString() {
return string.Format("EdgeStatus[Constraints={2},Anchor={0:F2},Offset={1:F2}]",
FromAnchor, Offset, Constraint);
}
}
}
/// <summary>
/// The types of constraints which can be applied to components in a relative layout.
/// </summary>
internal enum RelativeConstraintType {
Unconstrained, ToComponent, ToAnchor, Locked
}
}

View File

@@ -0,0 +1,132 @@
/*
* 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 UnityEngine;
namespace PeterHan.PLib.UI.Layouts {
/// <summary>
/// Parameters used to store the dynamic data of an object during a relative layout.
/// </summary>
internal sealed class RelativeLayoutResults : RelativeLayoutParamsBase<GameObject> {
/// <summary>
/// A set of insets that are always zero.
/// </summary>
private static readonly RectOffset ZERO = new RectOffset();
/// <summary>
/// The instance parameters of the bottom edge's component.
/// </summary>
internal RelativeLayoutResults BottomParams { get; set; }
/// <summary>
/// The height of the component plus its margin box.
/// </summary>
internal float EffectiveHeight { get; private set; }
/// <summary>
/// The width of the component plus its margin box.
/// </summary>
internal float EffectiveWidth { get; private set; }
/// <summary>
/// The instance parameters of the left edge's component.
/// </summary>
internal RelativeLayoutResults LeftParams { get; set; }
/// <summary>
/// The preferred height at which this component will be laid out, unless both
/// edges are constrained.
/// </summary>
internal float PreferredHeight {
get {
return prefSize.y;
}
set {
prefSize.y = value;
EffectiveHeight = value + Insets.top + Insets.bottom;
}
}
/// <summary>
/// The preferred width at which this component will be laid out, unless both
/// edges are constrained.
/// </summary>
internal float PreferredWidth {
get {
return prefSize.x;
}
set {
prefSize.x = value;
EffectiveWidth = value + Insets.left + Insets.right;
}
}
/// <summary>
/// The instance parameters of the right edge's component.
/// </summary>
internal RelativeLayoutResults RightParams { get; set; }
/// <summary>
/// The instance parameters of the top edge's component.
/// </summary>
internal RelativeLayoutResults TopParams { get; set; }
/// <summary>
/// The object to lay out.
/// </summary>
internal RectTransform Transform { get; set; }
/// <summary>
/// Whether the size delta should be used in the X direction (as opposed to offsets).
/// </summary>
internal bool UseSizeDeltaX { get; set; }
/// <summary>
/// Whether the size delta should be used in the Y direction (as opposed to offsets).
/// </summary>
internal bool UseSizeDeltaY { get; set; }
/// <summary>
/// The preferred size of this component.
/// </summary>
private Vector2 prefSize;
internal RelativeLayoutResults(RectTransform transform, RelativeLayoutParams other) {
Transform = transform ?? throw new ArgumentNullException(nameof(transform));
if (other != null) {
BottomEdge.CopyFrom(other.BottomEdge);
TopEdge.CopyFrom(other.TopEdge);
RightEdge.CopyFrom(other.RightEdge);
LeftEdge.CopyFrom(other.LeftEdge);
Insets = other.Insets ?? ZERO;
OverrideSize = other.OverrideSize;
} else
Insets = ZERO;
BottomParams = LeftParams = TopParams = RightParams = null;
PreferredWidth = PreferredHeight = 0.0f;
UseSizeDeltaX = UseSizeDeltaY = false;
}
public override string ToString() {
var go = Transform.gameObject;
return string.Format("component={0} {1:F2}x{2:F2}", (go == null) ? "null" : go.
name, prefSize.x, prefSize.y);
}
}
}

View File

@@ -0,0 +1,401 @@
/*
* 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());
}
}
}