using System.Collections.Generic; using UnityEngine; namespace Flexalon { /// /// Singleton class which tracks and updates all FlexalonNodes in the scene. /// See [core concepts](/docs/coreConcepts) for more information. /// [ExecuteAlways, HelpURL("https://www.flexalon.com/docs/coreConcepts")] public class Flexalon : MonoBehaviour { [SerializeField] private bool _updateInEditMode = true; /// Determines if Flexalon should automatically update in edit mode. public bool UpdateInEditMode { get { return _updateInEditMode; } set { _updateInEditMode = value; } } [SerializeField] private bool _updateInPlayMode = true; /// Determines if Flexalon should automatically update in play mode. public bool UpdateInPlayMode { get { return _updateInPlayMode; } set { _updateInPlayMode = value; } } [SerializeField] private bool _skipInactiveObjects = true; /// Determines if Flexalon should automatically skip inactive gameObjects in a layout. public bool SkipInactiveObjects { get { return _skipInactiveObjects; } set { _skipInactiveObjects = value; } } [SerializeField] private GameObject _inputProvider = null; private InputProvider _input; /// /// Override the default InputProvider used by FlexalonInteractables to support other input devices. /// public InputProvider InputProvider { get => _input; set => _input = value; } /// /// Set of nodes representing GameObjects tracked by Flexalon. /// public IReadOnlyCollection Nodes => _nodes; private static Flexalon _instance; private HashSet _nodes = new HashSet(); private Dictionary _gameObjects = new Dictionary(); private DefaultTransformUpdater _defaultTransformUpdater = new DefaultTransformUpdater(); private HashSet _roots = new HashSet(); private static Vector3 _defaultSize = Vector3.one; private List _destroyed = new List(); private static bool _undoRedo = false; private static bool _recordFrameChanges = false; public static bool RecordFrameChanges { get => _recordFrameChanges && !_undoRedo; set => _recordFrameChanges = value; } /// Event invoked before Flexalon updates. public System.Action PreUpdate; /// Returns the singleton Flexalon component. /// The singleton Flexalon component, or null if it doesn't exist. public static Flexalon Get() { return _instance; } public static Flexalon GetOrCreate() { TryGetOrCreate(out _); return _instance; } /// Returns the singleton Flexalon component, or creates one if it doesn't exist. /// The singleton Flexalon component. internal static bool TryGetOrCreate(out Flexalon instance) { bool created = false; if (!_instance) { #if UNITY_2023_1_OR_NEWER _instance = FindFirstObjectByType(); #else _instance = FindObjectOfType(); #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(FlexalonGO); created = true; } else { FlexalonLog.Log("Flexalon Instance Found in Scene"); } } instance = _instance; return created; } /// Returns the FlexalonNode associated with the gameObject. /// The gameObject to get the FlexalonNode for. /// The FlexalonNode associated with the gameObject, or null if it doesn't exist. public static FlexalonNode GetNode(GameObject go) { if (_instance != null && _instance && _instance._gameObjects.TryGetValue(go, out var node)) { return node; } return null; } /// /// Returns the FlexalonNode associated with the gameObject, /// or creates it if it doesn't exist. /// /// The gameObject to get the FlexalonNode for. /// The FlexalonNode associated with the gameObject. 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()); _instance._gameObjects.Add(go, node); } else if (!node._result) { node.RefreshResult(); } return node; } /// Gets the current InputProvider used by FlexalonInteractables. public static InputProvider GetInputProvider() { GetOrCreate(); if (_instance) { if (_instance._input == null) { if (_instance._inputProvider) { _instance._input = _instance._inputProvider.GetComponent(); } if (_instance._input == null) { _instance._input = new FlexalonMouseInputProvider(); } } return _instance._input; } return null; } /// Marks every node and FlexalonComponent as dirty and calls UpdateDirtyNodes. public void ForceUpdate() { foreach (var node in _nodes) { foreach (var flexalonComponent in node.GameObject.GetComponents()) { flexalonComponent.MarkDirty(); } node.MarkDirty(); } UpdateDirtyNodes(); } #if UNITY_EDITOR internal static bool DisableUndoForTest = false; private static HashSet _avoidAddComponentUndo = new HashSet(); #endif /// Helper to ensure undo operation on AddComponent is handled correctly. public static T AddComponent(GameObject go) where T : Component { return (T)AddComponent(go, typeof(T)); } /// Helper to ensure undo operation on AddComponent is handled correctly. 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; } /// Updates all dirty nodes. 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(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 _children = new List(); public IReadOnlyList 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 _dependents; public TransformUpdater _transformUpdater; public List _modifiers = null; public IReadOnlyList Modifiers => _modifiers; public event System.Action 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(); } _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(); _hasResult = _result != null; if (!_hasResult) { _result = AddComponent(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(); } _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(); } } } } } }