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