339 lines
14 KiB
C#
339 lines
14 KiB
C#
/*
|
|
* Copyright 2022 Peter Han
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
* and associated documentation files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all copies or
|
|
* substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
using PeterHan.PLib.Core;
|
|
using PeterHan.PLib.UI.Layouts;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
using RelativeConfig = PeterHan.PLib.UI.Layouts.RelativeLayoutParamsBase<PeterHan.PLib.UI.
|
|
IUIComponent>;
|
|
|
|
namespace PeterHan.PLib.UI {
|
|
/// <summary>
|
|
/// A panel which lays out its components using relative constraints.
|
|
///
|
|
/// This layout manager is the fastest of all panels when laid out, especially since it
|
|
/// can function properly when frozen even on dynamically sized items. However, it is also
|
|
/// the most difficult to set up and cannot handle all layouts.
|
|
/// </summary>
|
|
public class PRelativePanel : PContainer, IDynamicSizable {
|
|
/// <summary>
|
|
/// Constraints for each object are stored here.
|
|
/// </summary>
|
|
private readonly IDictionary<IUIComponent, RelativeConfig> constraints;
|
|
|
|
public bool DynamicSize { get; set; }
|
|
|
|
public PRelativePanel() : this(null) {
|
|
DynamicSize = true;
|
|
}
|
|
|
|
public PRelativePanel(string name) : base(name ?? "RelativePanel") {
|
|
constraints = new Dictionary<IUIComponent, RelativeConfig>(16);
|
|
Margin = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a child to this panel. Children must be added to the panel before they are
|
|
/// referenced in a constraint.
|
|
/// </summary>
|
|
/// <param name="child">The child to add.</param>
|
|
/// <returns>This panel for call chaining.</returns>
|
|
public PRelativePanel AddChild(IUIComponent child) {
|
|
if (child == null)
|
|
throw new ArgumentNullException(nameof(child));
|
|
if (!constraints.ContainsKey(child))
|
|
constraints.Add(child, new RelativeConfig());
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a handler when this panel is realized.
|
|
/// </summary>
|
|
/// <param name="onRealize">The handler to invoke on realization.</param>
|
|
/// <returns>This panel for call chaining.</returns>
|
|
public PRelativePanel AddOnRealize(PUIDelegates.OnRealize onRealize) {
|
|
OnRealize += onRealize;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Anchors the component's pivot in the X axis to the specified anchor position.
|
|
/// The component will be laid out at its preferred (or overridden) width with its
|
|
/// pivot locked to the specified relative fraction of the parent component's width.
|
|
///
|
|
/// Any other existing left or right edge constraints will be overwritten. This method
|
|
/// is equivalent to setting both the left and right edges to the same fraction.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="anchor">The fraction to which to align the pivot, with 0.0f
|
|
/// being the left and 1.0f being the right.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel AnchorXAxis(IUIComponent item, float anchor = 0.5f) {
|
|
SetLeftEdge(item, fraction: anchor);
|
|
return SetRightEdge(item, fraction: anchor);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Anchors the component's pivot in the Y axis to the specified anchor position.
|
|
/// The component will be laid out at its preferred (or overridden) height with its
|
|
/// pivot locked to the specified relative fraction of the parent component's height.
|
|
///
|
|
/// Any other existing top or bottom edge constraints will be overwritten. This method
|
|
/// is equivalent to setting both the top and bottom edges to the same fraction.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="anchor">The fraction to which to align the pivot, with 0.0f
|
|
/// being the bottom and 1.0f being the top.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel AnchorYAxis(IUIComponent item, float anchor = 0.5f) {
|
|
SetTopEdge(item, fraction: anchor);
|
|
return SetBottomEdge(item, fraction: anchor);
|
|
}
|
|
|
|
public override GameObject Build() {
|
|
var panel = PUIElements.CreateUI(null, Name);
|
|
var mapping = DictionaryPool<IUIComponent, GameObject, PRelativePanel>.Allocate();
|
|
SetImage(panel);
|
|
// Realize each component and add them to the panel
|
|
foreach (var pair in constraints) {
|
|
var component = pair.Key;
|
|
var realized = component.Build();
|
|
realized.SetParent(panel);
|
|
// We were already guaranteed that there were no duplicate keys
|
|
mapping[component] = realized;
|
|
}
|
|
// Add layout component
|
|
var layout = panel.AddComponent<RelativeLayoutGroup>();
|
|
layout.Margin = Margin;
|
|
foreach (var pair in constraints) {
|
|
var realized = mapping[pair.Key];
|
|
var rawParams = pair.Value;
|
|
var newParams = new RelativeLayoutParams();
|
|
// Copy all of the settings
|
|
Resolve(newParams.TopEdge, rawParams.TopEdge, mapping);
|
|
Resolve(newParams.BottomEdge, rawParams.BottomEdge, mapping);
|
|
Resolve(newParams.LeftEdge, rawParams.LeftEdge, mapping);
|
|
Resolve(newParams.RightEdge, rawParams.RightEdge, mapping);
|
|
newParams.OverrideSize = rawParams.OverrideSize;
|
|
newParams.Insets = rawParams.Insets;
|
|
layout.SetRaw(realized, newParams);
|
|
}
|
|
if (!DynamicSize)
|
|
layout.LockLayout();
|
|
mapping.Recycle();
|
|
// Set flex size
|
|
layout.flexibleWidth = FlexSize.x;
|
|
layout.flexibleHeight = FlexSize.y;
|
|
InvokeRealize(panel);
|
|
return panel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the constraints for a component, or throws an exception if the component
|
|
/// has not yet been added.
|
|
/// </summary>
|
|
/// <param name="item">The unrealized component to look up.</param>
|
|
/// <returns>The constraints for that component.</returns>
|
|
/// <exception cref="ArgumentException">If the component has not yet been added to the panel.</exception>
|
|
private RelativeConfig GetOrThrow(IUIComponent item) {
|
|
if (item == null)
|
|
throw new ArgumentNullException(nameof(item));
|
|
if (!constraints.TryGetValue(item, out RelativeConfig value))
|
|
throw new ArgumentException("Components must be added to the panel before using them in a constraint");
|
|
return value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overrides the preferred size of a component. If set, instead of looking at layout
|
|
/// sizes of the component, the specified size will be used instead.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="size">The size to apply. Only dimensions greater than zero will be used.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel OverrideSize(IUIComponent item, Vector2 size) {
|
|
if (item != null)
|
|
GetOrThrow(item).OverrideSize = size;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts the edge settings configured in this component to settings for the
|
|
/// relative panel.
|
|
/// </summary>
|
|
/// <param name="dest">The location where the converted settings will be stored.</param>
|
|
/// <param name="status">The original component edge configuration.</param>
|
|
/// <param name="mapping">The mapping from PLib UI components to Unity objects.</param>
|
|
private void Resolve(RelativeLayoutParams.EdgeStatus dest, RelativeConfig.EdgeStatus
|
|
status, IDictionary<IUIComponent, GameObject> mapping) {
|
|
var c = status.FromComponent;
|
|
dest.FromAnchor = status.FromAnchor;
|
|
if (c != null)
|
|
dest.FromComponent = mapping[c];
|
|
dest.Constraint = status.Constraint;
|
|
dest.Offset = status.Offset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the bottom edge of a game object. If the fraction is supplied, the component
|
|
/// will be laid out with the bottom edge anchored to that fraction of the parent's
|
|
/// height. If a component is specified and no fraction is specified, the component
|
|
/// will be anchored with its bottom edge above the top edge of that component.
|
|
/// If neither is specified, all bottom edge constraints will be removed.
|
|
///
|
|
/// Any other existing bottom edge constraint will be overwritten.
|
|
///
|
|
/// Remember that +Y is in the upwards direction.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="fraction">The fraction to which to align the bottom edge, with 0.0f
|
|
/// being the bottom and 1.0f being the top.</param>
|
|
/// <param name="above">The game object which this component must be above.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel SetBottomEdge(IUIComponent item, float fraction = -1.0f,
|
|
IUIComponent above = null) {
|
|
if (item != null) {
|
|
if (above == item)
|
|
throw new ArgumentException("Component cannot refer directly to itself");
|
|
SetEdge(GetOrThrow(item).BottomEdge, fraction, above);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/// <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(RelativeConfig.EdgeStatus edge, float fraction,
|
|
IUIComponent 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the left edge of a game object. If the fraction is supplied, the component
|
|
/// will be laid out with the left edge anchored to that fraction of the parent's
|
|
/// width. If a component is specified and no fraction is specified, the component
|
|
/// will be anchored with its left edge to the right of that component.
|
|
/// If neither is specified, all left edge constraints will be removed.
|
|
///
|
|
/// Any other existing left edge constraint will be overwritten.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="fraction">The fraction to which to align the left edge, with 0.0f
|
|
/// being the left and 1.0f being the right.</param>
|
|
/// <param name="toLeft">The game object which this component must be to the right of.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel SetLeftEdge(IUIComponent item, float fraction = -1.0f,
|
|
IUIComponent toRight = null) {
|
|
if (item != null) {
|
|
if (toRight == item)
|
|
throw new ArgumentException("Component cannot refer directly to itself");
|
|
SetEdge(GetOrThrow(item).LeftEdge, fraction, toRight);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the insets of a component from its anchor points. A positive number insets the
|
|
/// component away from the edge, whereas a negative number out-sets the component
|
|
/// across the edge.
|
|
///
|
|
/// All components default to no insets.
|
|
///
|
|
/// Any reference to a component's edge using other constraints always refers to its
|
|
/// edge <b>before</b> insets are applied.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="insets">The insets to apply. If null, the insets will be set to zero.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel SetMargin(IUIComponent item, RectOffset insets) {
|
|
if (item != null)
|
|
GetOrThrow(item).Insets = insets;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the right edge of a game object. If the fraction is supplied, the component
|
|
/// will be laid out with the right edge anchored to that fraction of the parent's
|
|
/// width. If a component is specified and no fraction is specified, the component
|
|
/// will be anchored with its right edge to the left of that component.
|
|
/// If neither is specified, all right edge constraints will be removed.
|
|
///
|
|
/// Any other existing right edge constraint will be overwritten.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="fraction">The fraction to which to align the right edge, with 0.0f
|
|
/// being the left and 1.0f being the right.</param>
|
|
/// <param name="toLeft">The game object which this component must be to the left of.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel SetRightEdge(IUIComponent item, float fraction = -1.0f,
|
|
IUIComponent toLeft = null) {
|
|
if (item != null) {
|
|
if (toLeft == item)
|
|
throw new ArgumentException("Component cannot refer directly to itself");
|
|
SetEdge(GetOrThrow(item).RightEdge, fraction, toLeft);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the top edge of a game object. If the fraction is supplied, the component
|
|
/// will be laid out with the top edge anchored to that fraction of the parent's
|
|
/// height. If a component is specified and no fraction is specified, the component
|
|
/// will be anchored with its top edge above the bottom edge of that component.
|
|
/// If neither is specified, all top edge constraints will be removed.
|
|
///
|
|
/// Any other existing top edge constraint will be overwritten.
|
|
///
|
|
/// Remember that +Y is in the upwards direction.
|
|
/// </summary>
|
|
/// <param name="item">The component to adjust.</param>
|
|
/// <param name="fraction">The fraction to which to align the top edge, with 0.0f
|
|
/// being the bottom and 1.0f being the top.</param>
|
|
/// <param name="below">The game object which this component must be below.</param>
|
|
/// <returns>This object, for call chaining.</returns>
|
|
public PRelativePanel SetTopEdge(IUIComponent item, float fraction = -1.0f,
|
|
IUIComponent below = null) {
|
|
if (item != null) {
|
|
if (below == item)
|
|
throw new ArgumentException("Component cannot refer directly to itself");
|
|
SetEdge(GetOrThrow(item).TopEdge, fraction, below);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
public override string ToString() {
|
|
return string.Format("PRelativePanel[Name={0}]", Name);
|
|
}
|
|
}
|
|
}
|