Initial commit
This commit is contained in:
239
mod/PLibUI/Layouts/AbstractLayoutGroup.cs
Normal file
239
mod/PLibUI/Layouts/AbstractLayoutGroup.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
266
mod/PLibUI/Layouts/BoxLayoutGroup.cs
Normal file
266
mod/PLibUI/Layouts/BoxLayoutGroup.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
mod/PLibUI/Layouts/BoxLayoutParams.cs
Normal file
61
mod/PLibUI/Layouts/BoxLayoutParams.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
147
mod/PLibUI/Layouts/BoxLayoutResults.cs
Normal file
147
mod/PLibUI/Layouts/BoxLayoutResults.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
208
mod/PLibUI/Layouts/CardLayoutGroup.cs
Normal file
208
mod/PLibUI/Layouts/CardLayoutGroup.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
mod/PLibUI/Layouts/CardLayoutResults.cs
Normal file
66
mod/PLibUI/Layouts/CardLayoutResults.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
mod/PLibUI/Layouts/GridComponent.cs
Normal file
42
mod/PLibUI/Layouts/GridComponent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
166
mod/PLibUI/Layouts/GridComponentSpec.cs
Normal file
166
mod/PLibUI/Layouts/GridComponentSpec.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
304
mod/PLibUI/Layouts/GridLayoutResults.cs
Normal file
304
mod/PLibUI/Layouts/GridLayoutResults.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
349
mod/PLibUI/Layouts/PGridLayoutGroup.cs
Normal file
349
mod/PLibUI/Layouts/PGridLayoutGroup.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
292
mod/PLibUI/Layouts/RelativeLayoutGroup.cs
Normal file
292
mod/PLibUI/Layouts/RelativeLayoutGroup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
185
mod/PLibUI/Layouts/RelativeLayoutParams.cs
Normal file
185
mod/PLibUI/Layouts/RelativeLayoutParams.cs
Normal 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
|
||||
}
|
||||
}
|
||||
132
mod/PLibUI/Layouts/RelativeLayoutResults.cs
Normal file
132
mod/PLibUI/Layouts/RelativeLayoutResults.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
401
mod/PLibUI/Layouts/RelativeLayoutUtil.cs
Normal file
401
mod/PLibUI/Layouts/RelativeLayoutUtil.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user