using System.Collections.Generic;
|
using UnityEngine;
|
|
namespace Flexalon
|
{
|
/// <summary>
|
/// Singleton class which tracks and updates all FlexalonNodes in the scene.
|
/// See [core concepts](/docs/coreConcepts) for more information.
|
/// </summary>
|
[ExecuteAlways, HelpURL("https://www.flexalon.com/docs/coreConcepts")]
|
public class Flexalon : MonoBehaviour
|
{
|
[SerializeField]
|
private bool _updateInEditMode = true;
|
/// <summary> Determines if Flexalon should automatically update in edit mode. </summary>
|
public bool UpdateInEditMode
|
{
|
get { return _updateInEditMode; }
|
set { _updateInEditMode = value; }
|
}
|
|
[SerializeField]
|
private bool _updateInPlayMode = true;
|
/// <summary> Determines if Flexalon should automatically update in play mode. </summary>
|
public bool UpdateInPlayMode
|
{
|
get { return _updateInPlayMode; }
|
set { _updateInPlayMode = value; }
|
}
|
|
[SerializeField]
|
private bool _skipInactiveObjects = true;
|
/// <summary> Determines if Flexalon should automatically skip inactive gameObjects in a layout. </summary>
|
public bool SkipInactiveObjects
|
{
|
get { return _skipInactiveObjects; }
|
set { _skipInactiveObjects = value; }
|
}
|
|
[SerializeField]
|
private GameObject _inputProvider = null;
|
private InputProvider _input;
|
/// <summary>
|
/// Override the default InputProvider used by FlexalonInteractables to support other input devices.
|
/// </summary>
|
public InputProvider InputProvider {
|
get => _input;
|
set => _input = value;
|
}
|
|
/// <summary>
|
/// Set of nodes representing GameObjects tracked by Flexalon.
|
/// </summary>
|
public IReadOnlyCollection<FlexalonNode> Nodes => _nodes;
|
|
private static Flexalon _instance;
|
|
private HashSet<Node> _nodes = new HashSet<Node>();
|
private Dictionary<GameObject, Node> _gameObjects = new Dictionary<GameObject, Node>();
|
private DefaultTransformUpdater _defaultTransformUpdater = new DefaultTransformUpdater();
|
private HashSet<Node> _roots = new HashSet<Node>();
|
private static Vector3 _defaultSize = Vector3.one;
|
private List<GameObject> _destroyed = new List<GameObject>();
|
|
private static bool _undoRedo = false;
|
private static bool _recordFrameChanges = false;
|
public static bool RecordFrameChanges
|
{
|
get => _recordFrameChanges && !_undoRedo;
|
set => _recordFrameChanges = value;
|
}
|
|
/// <summary> Event invoked before Flexalon updates. </summary>
|
public System.Action PreUpdate;
|
|
/// <summary> Returns the singleton Flexalon component. </summary>
|
/// <returns> The singleton Flexalon component, or null if it doesn't exist. </returns>
|
public static Flexalon Get()
|
{
|
return _instance;
|
}
|
|
public static Flexalon GetOrCreate()
|
{
|
TryGetOrCreate(out _);
|
return _instance;
|
}
|
|
/// <summary> Returns the singleton Flexalon component, or creates one if it doesn't exist. </summary>
|
/// <returns> The singleton Flexalon component. </returns>
|
internal static bool TryGetOrCreate(out Flexalon instance)
|
{
|
bool created = false;
|
if (!_instance)
|
{
|
#if UNITY_2023_1_OR_NEWER
|
_instance = FindFirstObjectByType<Flexalon>();
|
#else
|
_instance = FindObjectOfType<Flexalon>();
|
#endif
|
if (!_instance)
|
{
|
FlexalonLog.Log("New Flexalon Instance Created");
|
var FlexalonGO = new GameObject("Flexalon");
|
#if UNITY_EDITOR
|
UnityEditor.Undo.RegisterCreatedObjectUndo(FlexalonGO, "Create Flexalon");
|
#endif
|
_instance = AddComponent<Flexalon>(FlexalonGO);
|
created = true;
|
}
|
else
|
{
|
FlexalonLog.Log("Flexalon Instance Found in Scene");
|
}
|
}
|
|
instance = _instance;
|
return created;
|
}
|
|
/// <summary> Returns the FlexalonNode associated with the gameObject. </summary>
|
/// <param name="go"> The gameObject to get the FlexalonNode for. </param>
|
/// <returns> The FlexalonNode associated with the gameObject, or null if it doesn't exist. </returns>
|
public static FlexalonNode GetNode(GameObject go)
|
{
|
if (_instance != null && _instance && _instance._gameObjects.TryGetValue(go, out var node))
|
{
|
return node;
|
}
|
|
return null;
|
}
|
|
/// <summary>
|
/// Returns the FlexalonNode associated with the gameObject,
|
/// or creates it if it doesn't exist.
|
/// </summary>
|
/// <param name="go"> The gameObject to get the FlexalonNode for. </param>
|
/// <returns> The FlexalonNode associated with the gameObject. </returns>
|
public static FlexalonNode GetOrCreateNode(GameObject go)
|
{
|
if (go == null)
|
{
|
return null;
|
}
|
|
GetOrCreate();
|
|
if (!_instance._gameObjects.TryGetValue(go, out var node))
|
{
|
node = _instance.CreateNode();
|
node._gameObject = go;
|
node.RefreshResult();
|
node.SetResultToCurrentTransform();
|
|
// If inactive or disabled, FlexalonObject won't register itself, so do it here.
|
node.SetFlexalonObject(go.GetComponent<FlexalonObject>());
|
|
_instance._gameObjects.Add(go, node);
|
}
|
else if (!node._result)
|
{
|
node.RefreshResult();
|
}
|
|
return node;
|
}
|
|
/// <summary> Gets the current InputProvider used by FlexalonInteractables. </summary>
|
public static InputProvider GetInputProvider()
|
{
|
GetOrCreate();
|
|
if (_instance)
|
{
|
if (_instance._input == null)
|
{
|
if (_instance._inputProvider)
|
{
|
_instance._input = _instance._inputProvider.GetComponent<InputProvider>();
|
}
|
|
if (_instance._input == null)
|
{
|
_instance._input = new FlexalonMouseInputProvider();
|
}
|
}
|
|
return _instance._input;
|
}
|
|
return null;
|
}
|
|
/// <summary> Marks every node and FlexalonComponent as dirty and calls UpdateDirtyNodes. </summary>
|
public void ForceUpdate()
|
{
|
foreach (var node in _nodes)
|
{
|
foreach (var flexalonComponent in node.GameObject.GetComponents<FlexalonComponent>())
|
{
|
flexalonComponent.MarkDirty();
|
}
|
|
node.MarkDirty();
|
}
|
|
UpdateDirtyNodes();
|
}
|
|
#if UNITY_EDITOR
|
internal static bool DisableUndoForTest = false;
|
private static HashSet<GameObject> _avoidAddComponentUndo = new HashSet<GameObject>();
|
#endif
|
|
/// <summary> Helper to ensure undo operation on AddComponent is handled correctly. </summary>
|
public static T AddComponent<T>(GameObject go) where T : Component
|
{
|
return (T)AddComponent(go, typeof(T));
|
}
|
|
/// <summary> Helper to ensure undo operation on AddComponent is handled correctly. </summary>
|
public static Component AddComponent(GameObject go, System.Type type)
|
{
|
#if UNITY_EDITOR
|
if (!DisableUndoForTest && !_avoidAddComponentUndo.Contains(go))
|
{
|
// Avoid recursively recording undo because it
|
// causes warnings in some versions of Unity.
|
_avoidAddComponentUndo.Add(go);
|
// Debug.Log("AddComponent " + type.Name + " to " + go.name + " WITH UNDO");
|
var c = UnityEditor.Undo.AddComponent(go, type);
|
_avoidAddComponentUndo.Remove(go);
|
return c;
|
}
|
else
|
{
|
// Debug.Log("AddComponent " + type.Name + " to " + go.name);
|
return go.AddComponent(type);
|
}
|
#else
|
return go.AddComponent(type);
|
#endif
|
}
|
|
private Node CreateNode()
|
{
|
var node = new Node();
|
node._transformUpdater = _defaultTransformUpdater;
|
_nodes.Add(node);
|
_roots.Add(node);
|
return node;
|
}
|
|
private void DestroyNode(GameObject go)
|
{
|
if (_instance != null && _instance._gameObjects.TryGetValue(go, out var node))
|
{
|
_instance._gameObjects.Remove(go);
|
node.Detach();
|
node.DetachAllChildren();
|
node.SetDependency(null);
|
node.ClearDependents();
|
_nodes.Remove(node);
|
_roots.Remove(node);
|
}
|
}
|
|
void LateUpdate()
|
{
|
if (_instance != this)
|
{
|
return;
|
}
|
|
if (Application.isPlaying && _updateInPlayMode)
|
{
|
UpdateDirtyNodes();
|
}
|
|
if (!Application.isPlaying && _updateInEditMode)
|
{
|
UpdateDirtyNodes();
|
}
|
|
_undoRedo = false;
|
RecordFrameChanges = false;
|
}
|
|
/// <summary> Updates all dirty nodes. </summary>
|
public void UpdateDirtyNodes()
|
{
|
PreUpdate?.Invoke();
|
|
_destroyed.Clear();
|
foreach (var kv in _gameObjects)
|
{
|
var go = kv.Key;
|
var node = kv.Value;
|
if (!go)
|
{
|
_destroyed.Add(go);
|
}
|
else
|
{
|
if (!Application.isPlaying &&
|
(node._parent != null || node._dependency != null || node.HasFlexalonObject || node.Method != null))
|
{
|
node.CheckDefaultAdapter();
|
}
|
|
node.DetectRectTransformChanged();
|
}
|
}
|
|
foreach (var go in _destroyed)
|
{
|
DestroyNode(go);
|
}
|
|
foreach (var root in _roots)
|
{
|
if (root._dependency == null && root.GameObject.activeInHierarchy)
|
{
|
root.UpdateRootFillSize();
|
Compute(root);
|
}
|
}
|
|
_undoRedo = false;
|
RecordFrameChanges = false;
|
}
|
|
private void UpdateTransforms(Node node)
|
{
|
var rectTransform = node.GameObject.transform as RectTransform;
|
if (rectTransform != null && !node.ReachedTargetPosition)
|
{
|
rectTransform.anchorMin = rectTransform.anchorMax = rectTransform.pivot = new Vector2(0.5f, 0.5f);
|
}
|
|
if (!node.ReachedTargetPosition)
|
{
|
node.ReachedTargetPosition = node._transformUpdater.UpdatePosition(node, node._result.TargetPosition);
|
foreach (Node child in node._children)
|
{
|
child.ReachedTargetPosition = false;
|
}
|
}
|
|
if (!node.ReachedTargetRotation)
|
{
|
node.ReachedTargetRotation = node._transformUpdater.UpdateRotation(node, node._result.TargetRotation);
|
foreach (Node child in node._children)
|
{
|
child.ReachedTargetRotation = false;
|
}
|
}
|
|
if (!node.ReachedTargetScale)
|
{
|
node.ReachedTargetScale = node._transformUpdater.UpdateScale(node, node._result.TargetScale);
|
foreach (Node child in node._children)
|
{
|
child.ReachedTargetScale = false;
|
}
|
}
|
|
if (!node.ReachedTargetRectSize)
|
{
|
node.ReachedTargetRectSize = node._transformUpdater.UpdateRectSize(node, node._result.TargetRectSize);
|
}
|
|
node._result.TransformPosition = node.GameObject.transform.localPosition;
|
node._result.TransformRotation = node.GameObject.transform.localRotation;
|
node._result.TransformScale = node.GameObject.transform.localScale;
|
|
if (rectTransform != null)
|
{
|
node._result.TransformRectSize = rectTransform.rect.size;
|
}
|
|
node.NotifyResultChanged();
|
|
foreach (var child in node._children)
|
{
|
UpdateTransforms(child);
|
}
|
}
|
|
void Awake()
|
{
|
if (_instance == this)
|
{
|
RecordFrameChanges = false;
|
}
|
}
|
|
void OnEnable()
|
{
|
#if UNITY_EDITOR
|
if (_instance == this)
|
{
|
UnityEditor.Undo.undoRedoPerformed += OnUndoRedo;
|
}
|
#endif
|
}
|
|
void OnDisable()
|
{
|
#if UNITY_EDITOR
|
if (_instance == this)
|
{
|
UnityEditor.Undo.undoRedoPerformed -= OnUndoRedo;
|
}
|
#endif
|
}
|
|
void OnDestroy()
|
{
|
if (_instance == this)
|
{
|
FlexalonLog.Log("Flexalon Instance Destroyed");
|
_instance = null;
|
}
|
}
|
|
private void OnUndoRedo()
|
{
|
_undoRedo = true;
|
}
|
|
private void Compute(Node node)
|
{
|
if (node.Dirty && !node.IsDragging)
|
{
|
FlexalonLog.Log("LAYOUT COMPUTE", node);
|
MeasureRoot(node);
|
Arrange(node);
|
Constrain(node);
|
}
|
|
if (node.HasResult)
|
{
|
ComputeTransforms(node);
|
UpdateTransforms(node);
|
ComputeDependents(node);
|
}
|
}
|
|
private void ComputeDependents(Node node)
|
{
|
if (node._dependents != null)
|
{
|
var fillSize = Math.Mul(node._result.AdapterBounds.size, node.GetWorldBoxScale(true));
|
foreach (var dep in node._dependents)
|
{
|
if (dep.GameObject)
|
{
|
dep._dirty = dep._dirty || node.UpdateDependents;
|
|
var fillSizeForDep = fillSize;
|
if (dep.GameObject.transform.parent)
|
{
|
fillSizeForDep = Math.Div(fillSize, dep.GameObject.transform.parent.lossyScale);
|
}
|
|
dep.SetFillSize(fillSizeForDep);
|
Compute(dep);
|
}
|
}
|
}
|
|
node.UpdateDependents = false;
|
|
foreach (var child in node._children)
|
{
|
ComputeDependents(child);
|
}
|
}
|
|
private static Vector3 GetChildAvailableSize(Node node)
|
{
|
return Vector3.Max(Vector3.zero,
|
node._result.AdapterBounds.size - node.Padding.Size);
|
}
|
|
private void MeasureAdapterSize(Node node, Vector3 center, Vector3 size, Vector3 min, Vector3 max)
|
{
|
var adapterBounds = node.Adapter.Measure(node, size, min, max);
|
|
node.RecordResultUndo();
|
node._result.AdapterBounds = adapterBounds;
|
FlexalonLog.Log("MeasureAdapterSize", node, adapterBounds);
|
|
node._result.LayoutBounds = new Bounds(adapterBounds.center + center, adapterBounds.size);
|
FlexalonLog.Log("LayoutBounds", node, node._result.LayoutBounds);
|
}
|
|
private void MeasureRoot(Node node)
|
{
|
Vector3 min, max;
|
|
if (node.GameObject.transform.parent && node.GameObject.transform.parent is RectTransform parentRect)
|
{
|
min = node.GetMinSize(parentRect.rect.size, false);
|
max = node.GetMaxSize(parentRect.rect.size, false);
|
}
|
else
|
{
|
min = node.GetMinSize(Vector3.zero, false);
|
max = node.GetMaxSize(Math.MaxVector, false);
|
}
|
|
Measure(node, min, max, true);
|
}
|
|
private void MeasureChild(Node node, bool includeChildren = true)
|
{
|
var min = node.GetMinSize(Vector3.zero, false);
|
var max = node.GetMaxSize(Math.MaxVector, false);
|
Measure(node, min, max, includeChildren);
|
}
|
|
private void MeasureChild(Node node, Vector3 parentLayoutSize, bool includeChildren)
|
{
|
var min = node.GetMinSize(parentLayoutSize, false);
|
var max = Vector3.Min(node._result.ShrinkSize, node.GetMaxSize(parentLayoutSize, false));
|
Measure(node, min, max, includeChildren);
|
}
|
|
private void Measure(Node node, Vector3 min, Vector3 max, bool includeChildren = true)
|
{
|
FlexalonLog.Log($"Measure | {node.GameObject.name} {min} {max} {includeChildren}");
|
|
node.RecordResultUndo();
|
|
// Start by measuring whatever size we can. This might change after we
|
// run the layout method later if the size is set to children.
|
var size = MeasureSize(node);
|
MeasureAdapterSize(node, Vector3.zero, size, min, max);
|
|
if (includeChildren && node.Method != null)
|
{
|
MeasureLayout(node, min, max);
|
}
|
|
node.ApplyScaleAndRotation();
|
}
|
|
private void MeasureLayout(Node node, Vector3 min, Vector3 max)
|
{
|
// Now let the children run their measure before running our own.
|
// Assume empty fill size for now just to gather fixed and component values.
|
foreach (var child in node._children)
|
{
|
bool wasShrunk = child.IsShrunk();
|
child.ResetShrinkFillSize();
|
child.ResetFillShrinkChanged();
|
|
if (AnyAxisIsFill(child))
|
{
|
MeasureChild(child, false);
|
}
|
else if (child.Dirty || !child.HasResult || wasShrunk)
|
{
|
MeasureChild(child);
|
}
|
}
|
|
// Figure out how much space we have for the children
|
var childAvailableSize = GetChildAvailableSize(node);
|
|
var minChildAvailableSize = Vector3.Max(Vector3.zero, min - node.Padding.Size);
|
var maxChildAvailableSize = Vector3.Max(Vector3.zero, max - node.Padding.Size);
|
childAvailableSize = Math.Clamp(childAvailableSize, minChildAvailableSize, maxChildAvailableSize);
|
FlexalonLog.Log("Measure | ChildAvailableSize", node, childAvailableSize, minChildAvailableSize, maxChildAvailableSize);
|
|
// Measure what this node's size is given child sizes.
|
var layoutBounds = node.Method.Measure(node, childAvailableSize, minChildAvailableSize, maxChildAvailableSize);
|
FlexalonLog.Log("Measure | LayoutBounds 1", node, layoutBounds);
|
MeasureAdapterSize(node, layoutBounds.center, layoutBounds.size + node.Padding.Size, min, max);
|
|
// Measure any children that depend on our size
|
bool anyChildSizeChanged = false;
|
foreach (var child in node._children)
|
{
|
if (AnyAxisIsFill(child) || child.IsShrunk())
|
{
|
var previousSize = child.GetArrangeSize();
|
|
MeasureChild(child, layoutBounds.size, true);
|
|
if (previousSize != child.GetArrangeSize())
|
{
|
anyChildSizeChanged = true;
|
child._dirty = true;
|
}
|
|
child.ResetFillShrinkChanged();
|
}
|
}
|
|
if (anyChildSizeChanged)
|
{
|
// Re-measure given new child sizes.
|
layoutBounds = node.Method.Measure(node, childAvailableSize, minChildAvailableSize, maxChildAvailableSize);
|
FlexalonLog.Log("Measure | LayoutBounds 2", node, layoutBounds);
|
MeasureAdapterSize(node, layoutBounds.center, layoutBounds.size + node.Padding.Size, min, max);
|
|
// Measure any children that depend on our size in case it was wrong the first time.
|
// This cycle can continue forever, but this is the last time we'll do it.
|
foreach (var child in node._children)
|
{
|
if (AnyFillOrShrinkSizeChanged(child))
|
{
|
MeasureChild(child, layoutBounds.size, true);
|
child._dirty = true;
|
}
|
}
|
}
|
}
|
|
private void Arrange(Node node)
|
{
|
node._dirty = false;
|
node._hasResult = true;
|
node.SetPositionResult(Vector3.zero);
|
node.SetRotationResult(Quaternion.identity);
|
|
// If there's no children, there's nothing left to do.
|
if (node.Children.Count == 0 || node.Method == null)
|
{
|
return;
|
}
|
|
FlexalonLog.Log("Arrange", node, node._result.AdapterBounds.size);
|
|
// Run child arrange algorithm
|
foreach (var child in node._children)
|
{
|
if (child._dirty)
|
{
|
Arrange(child);
|
}
|
}
|
|
// Figure out how much space we have for the children
|
var childAvailableSize = GetChildAvailableSize(node);
|
FlexalonLog.Log("Arrange | ChildAvailableSize", node, childAvailableSize);
|
|
// Run our arrange algorithm
|
node.Method.Arrange(node, childAvailableSize);
|
|
// Run any attached modifiers
|
if (node.Modifiers != null)
|
{
|
foreach (var modifier in node.Modifiers)
|
{
|
modifier.PostArrange(node);
|
}
|
}
|
}
|
|
private void ComputeScale(Node node)
|
{
|
bool canScale = true;
|
canScale = node.Adapter.TryGetScale(node, out var componentScale);
|
node.SetComponentScale(componentScale);
|
|
bool shouldScale = canScale && (node.Parent != null || node.HasFlexalonObject);
|
if (!shouldScale)
|
{
|
node.ReachedTargetScale = true;
|
return;
|
}
|
|
var scale = node.Result.ComponentScale;
|
if (node.Parent != null)
|
{
|
scale = Math.Div(scale, node.Parent.Result.ComponentScale);
|
}
|
|
FlexalonLog.Log("ComputeTransform:Scale", node, scale);
|
scale.Scale(node.Scale);
|
node.RecordResultUndo();
|
node._result.TargetScale = scale;
|
node.ReachedTargetScale = false;
|
}
|
|
private void ComputeRectSize(Node node)
|
{
|
if (node.GameObject.transform is RectTransform && node.Adapter.TryGetRectSize(node, out var rectSize))
|
{
|
node.RecordResultUndo();
|
node.Result.TargetRectSize = rectSize;
|
node.ReachedTargetRectSize = false;
|
}
|
else
|
{
|
node.ReachedTargetRectSize = true;
|
}
|
}
|
|
private void ComputeTransforms(Node node)
|
{
|
if (node.HasSizeUpdate)
|
{
|
node.HasSizeUpdate = false;
|
ComputeScale(node);
|
ComputeRectSize(node);
|
if (node.Parent != null || node.HasFlexalonObject)
|
{
|
foreach (var child in node._children)
|
{
|
child.HasSizeUpdate = true;
|
}
|
}
|
}
|
|
if (node.Dependency != null)
|
{
|
if (node.HasPositionUpdate)
|
{
|
var position = node._result.LayoutPosition;
|
FlexalonLog.Log("ComputeTransform:Constrait:LayoutPosition", node, position);
|
node.RecordResultUndo();
|
node._result.TargetPosition = position;
|
node.ReachedTargetPosition = false;
|
}
|
|
if (node.HasRotationUpdate)
|
{
|
node.RecordResultUndo();
|
node._result.TargetRotation = node._result.LayoutRotation * node.Rotation;
|
FlexalonLog.Log("ComputeTransform:Constrait:Rotation", node, node._result.TargetRotation);
|
node.ReachedTargetRotation = false;
|
}
|
}
|
else if (node.Parent != null)
|
{
|
if (node.HasRotationUpdate)
|
{
|
node.RecordResultUndo();
|
node._result.TargetRotation = node._result.LayoutRotation * node.Rotation;
|
FlexalonLog.Log("ComputeTransform:Layout:Rotation", node, node._result.TargetRotation);
|
node.ReachedTargetRotation = false;
|
}
|
|
if (node.HasPositionUpdate)
|
{
|
var position = node._result.LayoutPosition
|
- node._parent.Padding.Center
|
+ node._parent._result.AdapterBounds.center
|
- node.Margin.Center
|
- node._result.TargetRotation * node._result.RotatedAndScaledBounds.center
|
+ node.Offset;
|
|
position = Math.Div(position, node.Parent.Result.ComponentScale);
|
FlexalonLog.Log("ComputeTransform:Layout:Position", node, position);
|
node.RecordResultUndo();
|
node._result.TargetPosition = position;
|
node.ReachedTargetPosition = false;
|
}
|
}
|
else
|
{
|
node.ReachedTargetPosition = true;
|
node.ReachedTargetRotation = true;
|
}
|
|
node.HasPositionUpdate = false;
|
node.HasRotationUpdate = false;
|
|
node._transformUpdater.PreUpdate(node);
|
|
foreach (var child in node._children)
|
{
|
ComputeTransforms(child);
|
}
|
}
|
|
private void Constrain(Node node)
|
{
|
if (node.Constraint != null)
|
{
|
FlexalonLog.Log("Constrain", node);
|
node.Constraint.Constrain(node);
|
}
|
}
|
|
private static Vector3 MeasureSize(Node node)
|
{
|
Vector3 result = new Vector3();
|
for (int axis = 0; axis < 3; axis++)
|
{
|
var unit = node.GetSizeType(axis);
|
if (unit == SizeType.Layout)
|
{
|
result[axis] = node._method != null ? node.Padding.Size[axis] : 0;
|
}
|
else if (unit == SizeType.Component)
|
{
|
result[axis] = 0;
|
}
|
else if (unit == SizeType.Fill)
|
{
|
var scale = node.Scale[axis];
|
var inverseScale = scale == 0 ? 0 : 1f / scale;
|
result[axis] = (node.Result.FillSize[axis] * node.SizeOfParent[axis] * inverseScale) - node.Margin.Size[axis];
|
}
|
else
|
{
|
result[axis] = node.Size[axis];
|
}
|
}
|
|
FlexalonLog.Log("MeasureSize", node, result);
|
return result;
|
}
|
|
private static bool AnyAxisIsFill(Node child)
|
{
|
return AxisIsFill(child, 0) || AxisIsFill(child, 1) || AxisIsFill(child, 2);
|
}
|
|
private static bool AxisIsFill(Node child, int axis)
|
{
|
return child.GetSizeType(axis) == SizeType.Fill ||
|
child.GetMaxSizeType(axis) == MinMaxSizeType.Fill ||
|
child.GetMinSizeType(axis) == MinMaxSizeType.Fill;
|
}
|
|
private static bool FillSizeChanged(Node child, int axis)
|
{
|
return AxisIsFill(child, axis) && child.FillSizeChanged[axis];
|
}
|
|
private static bool AnyFillSizeChanged(Node child)
|
{
|
return FillSizeChanged(child, 0) ||
|
FillSizeChanged(child, 1) ||
|
FillSizeChanged(child, 2);
|
}
|
|
private static bool ShrinkSizeChanged(Node child, int axis)
|
{
|
return child.CanShrink(axis) && child.ShrinkSizeChanged[axis];
|
}
|
|
private static bool AnyShrinkSizeChanged(Node child)
|
{
|
return ShrinkSizeChanged(child, 0) ||
|
ShrinkSizeChanged(child, 1) ||
|
ShrinkSizeChanged(child, 2);
|
}
|
|
private static bool AnyFillOrShrinkSizeChanged(Node child)
|
{
|
return AnyFillSizeChanged(child) || AnyShrinkSizeChanged(child);
|
}
|
|
internal static bool IsRootCanvas(GameObject go)
|
{
|
#if UNITY_UI
|
if (go.TryGetComponent<Canvas>(out var canvas))
|
{
|
return canvas.isRootCanvas;
|
}
|
#endif
|
return false;
|
}
|
|
private class Node : FlexalonNode
|
{
|
public Node _parent;
|
public FlexalonNode Parent => _parent;
|
public int _index;
|
public int Index => _index;
|
public List<Node> _children = new List<Node>();
|
public IReadOnlyList<FlexalonNode> Children => _children;
|
public bool _dirty = false;
|
public bool Dirty => _dirty;
|
public bool _hasResult = false;
|
public bool HasResult => _hasResult;
|
public bool HasPositionUpdate = false;
|
public bool HasSizeUpdate = false;
|
public bool HasRotationUpdate = false;
|
public bool ReachedTargetPosition = true;
|
public bool ReachedTargetRotation = true;
|
public bool ReachedTargetScale = true;
|
public bool ReachedTargetRectSize = true;
|
public bool UpdateDependents = false;
|
|
public GameObject _gameObject;
|
public GameObject GameObject => _gameObject;
|
public Layout _method;
|
public Layout Method { get => _method; set => _method = value; }
|
public Constraint _constraint;
|
public Constraint Constraint => _constraint;
|
private Adapter _adapter = null;
|
public Adapter Adapter => (_adapter == null) ? _adapter = new DefaultAdapter(GameObject) : _adapter;
|
public bool _customAdapter = false;
|
public FlexalonResult _result;
|
public FlexalonResult Result => _result;
|
public FlexalonObject _flexalonObject;
|
public FlexalonObject FlexalonObject => _flexalonObject;
|
public Vector3 Size => HasFlexalonObject ? _flexalonObject.Size : Vector3.one;
|
public Vector3 SizeOfParent => HasFlexalonObject ? _flexalonObject.SizeOfParent : Vector3.one;
|
public Vector3 Offset => HasFlexalonObject ? _flexalonObject.Offset : Vector3.zero;
|
public Vector3 Scale => HasFlexalonObject ? _flexalonObject.Scale : Vector3.one;
|
public Quaternion Rotation => HasFlexalonObject ? _flexalonObject.Rotation : Quaternion.identity;
|
public Directions Margin => HasFlexalonObject ? _flexalonObject.Margin : Directions.zero;
|
public Directions Padding => HasFlexalonObject ? _flexalonObject.Padding : Directions.zero;
|
public Node _dependency;
|
public FlexalonNode Dependency => _dependency;
|
public bool HasDependents => _dependents != null && _dependents.Count > 0;
|
public List<Node> _dependents;
|
public TransformUpdater _transformUpdater;
|
public List<FlexalonModifier> _modifiers = null;
|
public IReadOnlyList<FlexalonModifier> Modifiers => _modifiers;
|
public event System.Action<FlexalonNode> ResultChanged;
|
public bool IsDragging { get; set; }
|
private bool SkipInactive => _instance._skipInactiveObjects && !_gameObject.activeInHierarchy;
|
public bool SkipLayout => SkipInactive || (HasFlexalonObject ? _flexalonObject.SkipLayout : false);
|
private bool _hasFlexalonObject;
|
public bool HasFlexalonObject => _hasFlexalonObject;
|
public bool[] FillSizeChanged = new bool[3];
|
public bool[] ShrinkSizeChanged = new bool[3];
|
|
public void SetFillSize(Vector3 fillSize)
|
{
|
SetFillSize(0, fillSize.x);
|
SetFillSize(1, fillSize.y);
|
SetFillSize(2, fillSize.z);
|
}
|
|
public void SetFillSize(int axis, float size)
|
{
|
FlexalonLog.Log("SetFillSize", this, axis, size);
|
FillSizeChanged[axis] = FillSizeChanged[axis] || (_result.FillSize[axis] != size);
|
_result.FillSize[axis] = size;
|
}
|
|
public void ResetFillShrinkChanged()
|
{
|
FillSizeChanged[0] = FillSizeChanged[1] = FillSizeChanged[2] = false;
|
ShrinkSizeChanged[0] = ShrinkSizeChanged[1] = ShrinkSizeChanged[2] = false;
|
}
|
|
public void ResetShrinkFillSize()
|
{
|
_result.ShrinkSize = Math.MaxVector;
|
_result.FillSize = Vector3.zero;
|
}
|
|
public void SetShrinkSize(int axis, float size)
|
{
|
FlexalonLog.Log("SetShrinkSize", this, axis, size);
|
ShrinkSizeChanged[axis] = ShrinkSizeChanged[axis] || (_result.ShrinkSize[axis] != size);
|
_result.ShrinkSize[axis] = size;
|
}
|
|
public void SetShrinkFillSize(Vector3 childSize, Vector3 layoutSize, bool includesSizeOfParent)
|
{
|
for (int axis = 0; axis < 3; axis++)
|
{
|
SetShrinkFillSize(axis, childSize[axis], layoutSize[axis], includesSizeOfParent);
|
}
|
}
|
|
public void SetShrinkFillSize(int axis, float childSize, float layoutSize, bool includesSizeOfParent)
|
{
|
if (AxisIsFill(this, axis))
|
{
|
var fillSize = includesSizeOfParent ?
|
(SizeOfParent[axis] > 0 ? childSize / SizeOfParent[axis] : 0) :
|
childSize;
|
SetFillSize(axis, fillSize);
|
}
|
|
if (GetMinSizeType(axis) != MinMaxSizeType.None)
|
{
|
var measureSize = GetMeasureSize(axis, layoutSize);
|
if (measureSize > childSize)
|
{
|
SetShrinkSize(axis, Mathf.Max(GetMinSize(axis, layoutSize), childSize));
|
}
|
}
|
}
|
|
public bool IsShrunk()
|
{
|
return _result.ShrinkSize != Math.MaxVector;
|
}
|
|
public bool CanShrink(int axis)
|
{
|
return GetSizeType(axis) != SizeType.Fill && GetMinSizeType(axis) != MinMaxSizeType.None;
|
}
|
|
public void UpdateRootFillSize()
|
{
|
var newSize = GetRootFillSize();
|
if (newSize != _result.FillSize)
|
{
|
FlexalonLog.Log("UpdateRootFillSize", this, newSize);
|
SetFillSize(newSize);
|
MarkDirty();
|
}
|
}
|
|
private Vector3 GetRootFillSize()
|
{
|
var fillSize = _defaultSize;
|
if (GameObject.transform.parent && GameObject.transform.parent is RectTransform parentRect)
|
{
|
fillSize = parentRect.rect.size;
|
}
|
|
return fillSize;
|
}
|
|
public SizeType GetSizeType(Axis axis)
|
{
|
if (HasFlexalonObject)
|
{
|
switch (axis)
|
{
|
case Axis.X: return _flexalonObject.WidthType;
|
case Axis.Y: return _flexalonObject.HeightType;
|
case Axis.Z: return _flexalonObject.DepthType;
|
}
|
}
|
|
return SizeType.Component;
|
}
|
|
public SizeType GetSizeType(int axis)
|
{
|
return GetSizeType((Axis)axis);
|
}
|
|
public MinMaxSizeType GetMinSizeType(int axis)
|
{
|
return GetMinSizeType((Axis)axis);
|
}
|
|
public MinMaxSizeType GetMinSizeType(Axis axis)
|
{
|
if (HasFlexalonObject)
|
{
|
switch (axis)
|
{
|
case Axis.X: return _flexalonObject.MinWidthType;
|
case Axis.Y: return _flexalonObject.MinHeightType;
|
case Axis.Z: return _flexalonObject.MinDepthType;
|
}
|
}
|
|
return MinMaxSizeType.None;
|
}
|
|
public float GetMinSize(int axis, float parentLayoutSize)
|
{
|
return GetMinSize((Axis)axis, parentLayoutSize);
|
}
|
|
public float GetMinSize(Axis axis, float parentLayoutSize)
|
{
|
return GetMinSize(axis, parentLayoutSize, true);
|
}
|
|
public float GetMinSize(Axis axis, float parentLayoutSize, bool withMargin)
|
{
|
var margin = withMargin ? Margin.Size[(int)axis] : 0;
|
|
if (HasFlexalonObject)
|
{
|
switch (GetMinSizeType(axis))
|
{
|
case MinMaxSizeType.None:
|
return margin;
|
case MinMaxSizeType.Fixed:
|
return Mathf.Max(_flexalonObject.MinSize[(int)axis], 0) + margin;
|
case MinMaxSizeType.Fill:
|
return Mathf.Max(_flexalonObject.MinSizeOfParent[(int)axis] * parentLayoutSize, 0);
|
}
|
}
|
|
return 0;
|
}
|
|
public Vector3 GetMinSize(Vector3 parentLayoutSize)
|
{
|
return GetMinSize(parentLayoutSize, true);
|
}
|
|
public Vector3 GetMinSize(Vector3 parentLayoutSize, bool withMargin)
|
{
|
return new Vector3(GetMinSize(Axis.X, parentLayoutSize.x, withMargin),
|
GetMinSize(Axis.Y, parentLayoutSize.y, withMargin),
|
GetMinSize(Axis.Z, parentLayoutSize.z, withMargin));
|
}
|
|
public MinMaxSizeType GetMaxSizeType(int axis)
|
{
|
return GetMaxSizeType((Axis)axis);
|
}
|
|
public MinMaxSizeType GetMaxSizeType(Axis axis)
|
{
|
if (HasFlexalonObject)
|
{
|
switch (axis)
|
{
|
case Axis.X: return _flexalonObject.MaxWidthType;
|
case Axis.Y: return _flexalonObject.MaxHeightType;
|
case Axis.Z: return _flexalonObject.MaxDepthType;
|
}
|
}
|
|
return MinMaxSizeType.None;
|
}
|
|
public Vector3 GetMaxSize(Vector3 parentLayoutSize)
|
{
|
return GetMaxSize(parentLayoutSize, true);
|
}
|
|
public float GetMaxSize(int axis, float parentLayoutSize)
|
{
|
return GetMaxSize((Axis)axis, parentLayoutSize);
|
}
|
|
public float GetMaxSize(Axis axis, float fillSize)
|
{
|
return GetMaxSize(axis, fillSize, true);
|
}
|
|
public float GetMaxSize(Axis axis, float parentLayoutSize, bool withMargin)
|
{
|
var margin = withMargin ? Margin.Size[(int)axis] : 0;
|
|
if (HasFlexalonObject)
|
{
|
switch (GetMaxSizeType(axis))
|
{
|
case MinMaxSizeType.None:
|
return Math.MaxValue;
|
case MinMaxSizeType.Fixed:
|
return Mathf.Max(0, _flexalonObject.MaxSize[(int)axis]) + margin;
|
case MinMaxSizeType.Fill:
|
return Mathf.Max(0, _flexalonObject.MaxSizeOfParent[(int)axis] * parentLayoutSize);
|
}
|
}
|
|
return Math.MaxValue;
|
}
|
|
public Vector3 GetMaxSize(Vector3 parentLayoutSize, bool withMargin)
|
{
|
return new Vector3(GetMaxSize(Axis.X, parentLayoutSize.x, withMargin),
|
GetMaxSize(Axis.Y, parentLayoutSize.y, withMargin),
|
GetMaxSize(Axis.Z, parentLayoutSize.z, withMargin));
|
}
|
|
public void SetPositionResult(Vector3 position)
|
{
|
RecordResultUndo();
|
_result.LayoutPosition = position;
|
HasPositionUpdate = true;
|
UpdateDependents = true;
|
}
|
|
public void SetRotationResult(Quaternion quaternion)
|
{
|
RecordResultUndo();
|
_result.LayoutRotation = quaternion;
|
HasRotationUpdate = true;
|
UpdateDependents = true;
|
}
|
|
public void SetComponentScale(Vector3 scale)
|
{
|
RecordResultUndo();
|
_result.ComponentScale = scale;
|
}
|
|
public void SetMethod(Layout method)
|
{
|
_method = method;
|
}
|
|
public void SetConstraint(Constraint constraint, FlexalonNode target)
|
{
|
_constraint = constraint;
|
SetDependency(target);
|
}
|
|
public void SetTransformUpdater(TransformUpdater updater)
|
{
|
updater = updater != null ? updater : _instance?._defaultTransformUpdater;
|
if (updater != _transformUpdater)
|
{
|
_transformUpdater = updater;
|
}
|
}
|
|
public void SetFlexalonObject(FlexalonObject obj)
|
{
|
_hasFlexalonObject = obj != null;
|
_flexalonObject = obj;
|
}
|
|
public void MarkDirty()
|
{
|
if (Dirty) return;
|
|
#if FLEXALON_LOG
|
var callStack = new System.Diagnostics.StackTrace().ToString();
|
if (!callStack.Contains("OnDestroy"))
|
{
|
FlexalonLog.Log("MarkDirty", this);
|
}
|
#endif
|
|
var node = this;
|
while (node != null)
|
{
|
node._dirty = true;
|
node.HasPositionUpdate = true;
|
node.HasRotationUpdate = true;
|
node.HasSizeUpdate = true;
|
node = node._parent;
|
}
|
|
if (_dependency != null && !_dependency.HasResult)
|
{
|
_dependency?.MarkDirty();
|
}
|
|
#if UNITY_EDITOR
|
UnityEditor.EditorApplication.QueuePlayerLoopUpdate();
|
#endif
|
}
|
|
public void ForceUpdate()
|
{
|
MarkDirty();
|
MarkDirtyDown();
|
Flexalon.GetOrCreate().UpdateDirtyNodes();
|
}
|
|
private void MarkDirtyDown()
|
{
|
foreach (var child in _children)
|
{
|
child.MarkDirty();
|
child.MarkDirtyDown();
|
}
|
|
if (HasDependents)
|
{
|
foreach (var dep in _dependents)
|
{
|
dep.MarkDirty();
|
dep.MarkDirtyDown();
|
}
|
}
|
}
|
|
public void AddChild(FlexalonNode child)
|
{
|
InsertChild(child, _children.Count);
|
}
|
|
public void InsertChild(FlexalonNode child, int index)
|
{
|
var childNode = child as Node;
|
if (childNode._parent == this && childNode._index == index)
|
{
|
return;
|
}
|
|
child.Detach();
|
|
childNode._parent = this;
|
childNode._index = index;
|
_children.Insert(index, childNode);
|
_instance?._roots.Remove(childNode);
|
}
|
|
public FlexalonNode GetChild(int index)
|
{
|
return _children[index];
|
}
|
|
public void Detach()
|
{
|
if (_parent != null)
|
{
|
_parent._children.Remove(this);
|
_parent = null;
|
_index = 0;
|
|
if (_instance != null && _instance._gameObjects.ContainsKey(GameObject))
|
{
|
_instance._roots.Add(this);
|
}
|
}
|
}
|
|
public void DetachAllChildren()
|
{
|
while (Children.Count > 0)
|
{
|
Children[Children.Count - 1].Detach();
|
}
|
}
|
|
public Vector3 GetMeasureSize(Vector3 layoutSize)
|
{
|
return new Vector3(GetMeasureSize(0, layoutSize.x),
|
GetMeasureSize(1, layoutSize.y),
|
GetMeasureSize(2, layoutSize.z));
|
}
|
|
public float GetMeasureSize(int axis, float layoutSize)
|
{
|
var size = GetSizeType(axis) == SizeType.Fill ? 0 : _result.RotatedAndScaledBounds.size[axis] + Margin.Size[axis];
|
return Mathf.Clamp(size, GetMinSize(axis, layoutSize), GetMaxSize(axis, layoutSize));
|
}
|
|
public Vector3 GetArrangeSize()
|
{
|
return _result.RotatedAndScaledBounds.size + Margin.Size;
|
}
|
|
public Vector3 GetBoxScale()
|
{
|
bool shouldScale = Adapter.TryGetScale(this, out var _);
|
if (!shouldScale)
|
{
|
return GameObject.transform.localScale;
|
}
|
else if (HasFlexalonObject)
|
{
|
// FlexalonObject size/scale always applies, even without a layout.
|
return _flexalonObject.Scale;
|
}
|
else if (_parent != null)
|
{
|
return Vector3.one;
|
}
|
else
|
{
|
return GameObject.transform.localScale;
|
}
|
}
|
|
public Quaternion GetBoxRotation()
|
{
|
// FlexalonObject rotation only takes effect if there's a layout.
|
if (_parent != null || _dependency != null)
|
{
|
return HasFlexalonObject ? _flexalonObject.Rotation : Quaternion.identity;
|
}
|
else
|
{
|
return GameObject.transform.localRotation;
|
}
|
}
|
|
public Vector3 GetWorldBoxScale(bool includeLocalScale)
|
{
|
Vector3 scale = includeLocalScale ? GetBoxScale() : Vector3.one;
|
var node = this;
|
while (node._parent != null)
|
{
|
scale.Scale(node._parent.GetBoxScale());
|
node = node._parent;
|
}
|
|
if (node.GameObject.transform.parent != null)
|
{
|
scale.Scale(node.GameObject.transform.parent.lossyScale);
|
}
|
|
return scale;
|
}
|
|
public Vector3 GetWorldBoxPosition(Vector3 scale, bool includePadding)
|
{
|
var pos = _result.LayoutBounds.center;
|
if (includePadding)
|
{
|
pos -= Padding.Center;
|
}
|
|
pos.Scale(scale);
|
pos = GameObject.transform.rotation * pos + GameObject.transform.position;
|
return pos;
|
}
|
|
public void SetDependency(FlexalonNode node)
|
{
|
if (_dependency != node)
|
{
|
_dependency?._dependents.Remove(this);
|
|
_dependency = node as Node;
|
|
if (node != null)
|
{
|
if (_dependency._dependents == null)
|
{
|
_dependency._dependents = new List<Node>();
|
}
|
|
_dependency._dependents.Add(this);
|
}
|
}
|
}
|
|
public void ClearDependents()
|
{
|
if (_dependents != null)
|
{
|
while (_dependents.Count > 0)
|
{
|
_dependents[_dependents.Count - 1].SetDependency(null);
|
}
|
}
|
}
|
|
public void SetAdapter(Adapter adapter)
|
{
|
_adapter = adapter;
|
_customAdapter = (_adapter != null);
|
}
|
|
public void CheckDefaultAdapter()
|
{
|
if (!_customAdapter)
|
{
|
if ((Adapter as DefaultAdapter).CheckComponent(GameObject))
|
{
|
MarkDirty();
|
}
|
}
|
}
|
|
public void ApplyScaleAndRotation()
|
{
|
var bounds = Math.ScaleBounds(_result.LayoutBounds, GetBoxScale());
|
bounds = Math.RotateBounds(bounds, GetBoxRotation());
|
RecordResultUndo();
|
_result.RotatedAndScaledBounds = bounds;
|
FlexalonLog.Log("Measure | RotatedAndScaledBounds", this, bounds);
|
HasSizeUpdate = true;
|
UpdateDependents = true;
|
}
|
|
public void RefreshResult()
|
{
|
_result = _gameObject.GetComponent<FlexalonResult>();
|
_hasResult = _result != null;
|
if (!_hasResult)
|
{
|
_result = AddComponent<FlexalonResult>(GameObject);
|
_dirty = true;
|
}
|
}
|
|
public void SetResultToCurrentTransform()
|
{
|
_result.TransformPosition = GameObject.transform.localPosition;
|
_result.TransformRotation = GameObject.transform.localRotation;
|
_result.TransformScale = GameObject.transform.localScale;
|
_result.TargetPosition = GameObject.transform.localPosition;
|
_result.TargetRotation = GameObject.transform.localRotation;
|
_result.TargetScale = GameObject.transform.localScale;
|
}
|
|
public void RecordResultUndo()
|
{
|
#if UNITY_EDITOR
|
if (Flexalon.RecordFrameChanges && _result != null)
|
{
|
UnityEditor.Undo.RecordObject(_result, "Result changed");
|
UnityEditor.PrefabUtility.RecordPrefabInstancePropertyModifications(_result);
|
}
|
#endif
|
}
|
|
public void AddModifier(FlexalonModifier modifier)
|
{
|
if (_modifiers == null)
|
{
|
_modifiers = new List<FlexalonModifier>();
|
}
|
|
_modifiers.RemoveAll(m => m == modifier);
|
_modifiers.Add(modifier);
|
}
|
|
public void RemoveModifier(FlexalonModifier modifier)
|
{
|
_modifiers?.Remove(modifier);
|
}
|
|
public void NotifyResultChanged()
|
{
|
ResultChanged?.Invoke(this);
|
}
|
|
public void DetectRectTransformChanged()
|
{
|
if (GameObject.transform is RectTransform rectTransform)
|
{
|
// Check if the rect size changed unexpectedly, either by the user or a UGUI component.
|
if (ReachedTargetRectSize && _result.TransformRectSize != rectTransform.rect.size)
|
{
|
MarkDirty();
|
}
|
}
|
}
|
}
|
}
|
}
|