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.9.3