From d2ab30e7a69bfe7efda63ae75812207377917bd3 Mon Sep 17 00:00:00 2001
From: miepzerino <o.skotnik@gmail.com>
Date: Sun, 30 Mar 2025 18:50:27 +0000
Subject: [PATCH] Merge branch 'Flexalon-UI-Layouts' into develop
---
Assets/Flexalon/Runtime/Core/Flexalon.cs | 1531 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 1,531 insertions(+), 0 deletions(-)
diff --git a/Assets/Flexalon/Runtime/Core/Flexalon.cs b/Assets/Flexalon/Runtime/Core/Flexalon.cs
new file mode 100644
index 0000000..828098d
--- /dev/null
+++ b/Assets/Flexalon/Runtime/Core/Flexalon.cs
@@ -0,0 +1,1531 @@
+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();
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
--
Gitblit v1.10.0