diff --git a/src/UI/InteractiveValues/InteractiveFlags.cs b/src/UI/InteractiveValues/InteractiveFlags.cs index 946db87..970bad0 100644 --- a/src/UI/InteractiveValues/InteractiveFlags.cs +++ b/src/UI/InteractiveValues/InteractiveFlags.cs @@ -48,6 +48,8 @@ namespace UnityExplorer.UI.InteractiveValues GetDefaultLabel(); m_baseLabel.text = DefaultLabel; + base.RefreshUIForValue(); + if (m_subContentConstructed) { for (int i = 0; i < m_values.Length; i++) diff --git a/src/UI/InteractiveValues/InteractiveFloatStruct.cs b/src/UI/InteractiveValues/InteractiveFloatStruct.cs new file mode 100644 index 0000000..689c650 --- /dev/null +++ b/src/UI/InteractiveValues/InteractiveFloatStruct.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using UnityEngine.UI; +using System.Reflection; + +namespace UnityExplorer.UI.InteractiveValues +{ + // Class for supporting any "float struct" (ie Vector, Rect, etc). + // Supports any struct where all the public instance fields are floats (or types assignable to float) + + public class StructInfo + { + public string[] FieldNames { get; } + private readonly FieldInfo[] m_fields; + + public StructInfo(Type type) + { + m_fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) + .Where(it => !it.IsLiteral) + .ToArray(); + + FieldNames = m_fields.Select(it => it.Name) + .ToArray(); + } + + public object SetValue(ref object instance, int fieldIndex, float val) + { + m_fields[fieldIndex].SetValue(instance, val); + return instance; + } + + public float GetValue(object instance, int fieldIndex) + => (float)m_fields[fieldIndex].GetValue(instance); + + public void RefreshUI(InputField[] inputs, object instance) + { + try + { + for (int i = 0; i < m_fields.Length; i++) + { + var field = m_fields[i]; + float val = (float)field.GetValue(instance); + inputs[i].text = val.ToString(); + } + } + catch (Exception ex) + { + ExplorerCore.Log(ex); + } + } + } + + public class InteractiveFloatStruct : InteractiveValue + { + private static readonly Dictionary _typeSupportCache = new Dictionary(); + public static bool IsTypeSupported(Type type) + { + if (!type.IsValueType) + return false; + + if (_typeSupportCache.TryGetValue(type.AssemblyQualifiedName, out bool ret)) + return ret; + + ret = true; + var fields = type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + foreach (var field in fields) + { + if (field.IsLiteral) + continue; + + if (!typeof(float).IsAssignableFrom(field.FieldType)) + { + ret = false; + break; + } + } + _typeSupportCache.Add(type.AssemblyQualifiedName, ret); + return ret; + } + + //~~~~~~~~~ Instance ~~~~~~~~~~ + + public InteractiveFloatStruct(object value, Type valueType) : base(value, valueType) { } + + public override bool HasSubContent => true; + public override bool SubContentWanted => true; + + public StructInfo StructInfo; + + public override void RefreshUIForValue() + { + InitializeStructInfo(); + + base.RefreshUIForValue(); + + if (m_subContentConstructed) + StructInfo.RefreshUI(m_inputs, this.Value); + } + + internal override void OnToggleSubcontent(bool toggle) + { + InitializeStructInfo(); + + base.OnToggleSubcontent(toggle); + + StructInfo.RefreshUI(m_inputs, this.Value); + } + + internal Type m_lastStructType; + + internal void InitializeStructInfo() + { + var type = Value?.GetType() ?? FallbackType; + + if (StructInfo != null && type == m_lastStructType) + return; + + if (StructInfo != null && m_subContentConstructed) + DestroySubContent(); + + m_lastStructType = type; + + StructInfo = new StructInfo(type); + + if (m_subContentParent.activeSelf) + ConstructSubcontent(); + } + + #region UI CONSTRUCTION + + internal InputField[] m_inputs; + + public override void ConstructUI(GameObject parent, GameObject subGroup) + { + base.ConstructUI(parent, subGroup); + } + + public override void ConstructSubcontent() + { + base.ConstructSubcontent(); + + if (StructInfo == null) + { + ExplorerCore.LogWarning("Setting up subcontent but structinfo is null"); + return; + } + + var editorContainer = UIFactory.CreateVerticalGroup(m_subContentParent, "EditorContent", false, true, true, true, 2, new Vector4(4, 4, 4, 4), + new Color(0.08f, 0.08f, 0.08f)); + + m_inputs = new InputField[StructInfo.FieldNames.Length]; + + for (int i = 0; i < StructInfo.FieldNames.Length; i++) + AddEditorRow(i, editorContainer); + + RefreshUIForValue(); + } + + internal void AddEditorRow(int index, GameObject groupObj) + { + try + { + var row = UIFactory.CreateHorizontalGroup(groupObj, "EditorRow", false, true, true, true, 5, default, new Color(1, 1, 1, 0)); + + string name = StructInfo.FieldNames[index]; + + var label = UIFactory.CreateLabel(row, "RowLabel", $"{name}:", TextAnchor.MiddleRight, Color.cyan); + UIFactory.SetLayoutElement(label.gameObject, minWidth: 30, flexibleWidth: 0, minHeight: 25); + + var inputFieldObj = UIFactory.CreateInputField(row, "InputField", "...", 14, 3, 1); + UIFactory.SetLayoutElement(inputFieldObj, minWidth: 120, minHeight: 25, flexibleWidth: 0); + + var inputField = inputFieldObj.GetComponent(); + m_inputs[index] = inputField; + + inputField.onValueChanged.AddListener((string val) => + { + try + { + float f = float.Parse(val); + Value = StructInfo.SetValue(ref this.Value, index, f); + Owner.SetValue(); + } + catch { } + }); + } + catch (Exception ex) + { + ExplorerCore.Log(ex); + } + } + + #endregion + } +} diff --git a/src/UI/InteractiveValues/InteractiveUnityStruct.cs b/src/UI/InteractiveValues/InteractiveUnityStruct.cs deleted file mode 100644 index 8eed042..0000000 --- a/src/UI/InteractiveValues/InteractiveUnityStruct.cs +++ /dev/null @@ -1,298 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using UnityEngine; -using UnityEngine.UI; -using UnityExplorer.Core.Unity; -using UnityExplorer.UI; - -namespace UnityExplorer.UI.InteractiveValues -{ - #region IStructInfo helper - - public interface IStructInfo - { - string[] FieldNames { get; } - object SetValue(ref object value, int fieldIndex, float val); - void RefreshUI(InputField[] inputs, object value); - } - - public class StructInfo : IStructInfo where T : struct - { - public string[] FieldNames { get; set; } - - public delegate void SetMethod(ref T value, int fieldIndex, float val); - public SetMethod SetValueMethod; - - public delegate void UpdateMethod(InputField[] inputs, object value); - public UpdateMethod UpdateUIMethod; - - public object SetValue(ref object value, int fieldIndex, float val) - { - var box = (T)value; - SetValueMethod.Invoke(ref box, fieldIndex, val); - return box; - } - - public void RefreshUI(InputField[] inputs, object value) - { - UpdateUIMethod.Invoke(inputs, value); - } - } - - // This part is a bit ugly, but everything else is generalized above. - // I could generalize it more with reflection, but it would be different for - // mono/il2cpp and also slower. - public static class StructInfoFactory - { - public static IStructInfo Create(Type type) - { - if (type == typeof(Vector2)) - { - return new StructInfo() - { - FieldNames = new[] { "x", "y", }, - SetValueMethod = (ref Vector2 vec, int fieldIndex, float val) => - { - switch (fieldIndex) - { - case 0: vec.x = val; break; - case 1: vec.y = val; break; - } - }, - UpdateUIMethod = (InputField[] inputs, object value) => - { - Vector2 vec = (Vector2)value; - inputs[0].text = vec.x.ToString(); - inputs[1].text = vec.y.ToString(); - } - }; - } - else if (type == typeof(Vector3)) - { - return new StructInfo() - { - FieldNames = new[] { "x", "y", "z" }, - SetValueMethod = (ref Vector3 vec, int fieldIndex, float val) => - { - switch (fieldIndex) - { - case 0: vec.x = val; break; - case 1: vec.y = val; break; - case 2: vec.z = val; break; - } - }, - UpdateUIMethod = (InputField[] inputs, object value) => - { - Vector3 vec = (Vector3)value; - inputs[0].text = vec.x.ToString(); - inputs[1].text = vec.y.ToString(); - inputs[2].text = vec.z.ToString(); - } - }; - } - else if (type == typeof(Vector4)) - { - return new StructInfo() - { - FieldNames = new[] { "x", "y", "z", "w" }, - SetValueMethod = (ref Vector4 vec, int fieldIndex, float val) => - { - switch (fieldIndex) - { - case 0: vec.x = val; break; - case 1: vec.y = val; break; - case 2: vec.z = val; break; - case 3: vec.w = val; break; - } - }, - UpdateUIMethod = (InputField[] inputs, object value) => - { - Vector4 vec = (Vector4)value; - inputs[0].text = vec.x.ToString(); - inputs[1].text = vec.y.ToString(); - inputs[2].text = vec.z.ToString(); - inputs[3].text = vec.w.ToString(); - } - }; - } - else if (type == typeof(Rect)) - { - return new StructInfo() - { - FieldNames = new[] { "x", "y", "width", "height" }, - SetValueMethod = (ref Rect vec, int fieldIndex, float val) => - { - switch (fieldIndex) - { - case 0: vec.x = val; break; - case 1: vec.y = val; break; - case 2: vec.width = val; break; - case 3: vec.height = val; break; - } - }, - UpdateUIMethod = (InputField[] inputs, object value) => - { - Rect vec = (Rect)value; - inputs[0].text = vec.x.ToString(); - inputs[1].text = vec.y.ToString(); - inputs[2].text = vec.width.ToString(); - inputs[3].text = vec.height.ToString(); - } - }; - } - else if (type == typeof(Color)) - { - return new StructInfo() - { - FieldNames = new[] { "r", "g", "b", "a" }, - SetValueMethod = (ref Color vec, int fieldIndex, float val) => - { - switch (fieldIndex) - { - case 0: vec.r = val; break; - case 1: vec.g = val; break; - case 2: vec.b = val; break; - case 3: vec.a = val; break; - } - }, - UpdateUIMethod = (InputField[] inputs, object value) => - { - Color vec = (Color)value; - inputs[0].text = vec.r.ToString(); - inputs[1].text = vec.g.ToString(); - inputs[2].text = vec.b.ToString(); - inputs[3].text = vec.a.ToString(); - } - }; - } - else - throw new NotImplementedException(); - } - } - - #endregion - - public class InteractiveUnityStruct : InteractiveValue - { - public static bool SupportsType(Type type) => s_supportedTypes.Contains(type); - private static readonly HashSet s_supportedTypes = new HashSet - { - typeof(Vector2), - typeof(Vector3), - typeof(Vector4), - typeof(Rect), - //typeof(Color) // todo might make a special editor for colors - }; - - //~~~~~~~~~ Instance ~~~~~~~~~~ - - public InteractiveUnityStruct(object value, Type valueType) : base(value, valueType) { } - - public override bool HasSubContent => true; - public override bool SubContentWanted => true; - public override bool WantInspectBtn => true; - - public IStructInfo StructInfo; - - public override void RefreshUIForValue() - { - InitializeStructInfo(); - - base.RefreshUIForValue(); - - if (m_subContentConstructed) - StructInfo.RefreshUI(m_inputs, this.Value); - } - - internal override void OnToggleSubcontent(bool toggle) - { - InitializeStructInfo(); - - base.OnToggleSubcontent(toggle); - - StructInfo.RefreshUI(m_inputs, this.Value); - } - - internal Type m_lastStructType; - - internal void InitializeStructInfo() - { - var type = Value?.GetType() ?? FallbackType; - - if (StructInfo != null && type == m_lastStructType) - return; - - if (StructInfo != null) - DestroySubContent(); - - m_lastStructType = type; - - StructInfo = StructInfoFactory.Create(type); - - if (m_subContentParent.activeSelf) - { - ConstructSubcontent(); - } - } - - #region UI CONSTRUCTION - - internal InputField[] m_inputs; - - public override void ConstructUI(GameObject parent, GameObject subGroup) - { - base.ConstructUI(parent, subGroup); - } - - public override void ConstructSubcontent() - { - base.ConstructSubcontent(); - - if (StructInfo == null) - { - ExplorerCore.LogWarning("Setting up subcontent but structinfo is null"); - return; - } - - var editorContainer = UIFactory.CreateVerticalGroup(m_subContentParent, "EditorContent", false, true, true, true, 2, new Vector4(4,4,4,4), - new Color(0.08f, 0.08f, 0.08f)); - - m_inputs = new InputField[StructInfo.FieldNames.Length]; - - for (int i = 0; i < StructInfo.FieldNames.Length; i++) - AddEditorRow(i, editorContainer); - - if (Owner.CanWrite) - { - var applyBtn = UIFactory.CreateButton(editorContainer, "ApplyButton", "Apply", OnSetValue, new Color(0.2f, 0.2f, 0.2f)); - UIFactory.SetLayoutElement(applyBtn.gameObject, minWidth: 175, minHeight: 25, flexibleWidth: 0); - - void OnSetValue() - { - Owner.SetValue(); - RefreshUIForValue(); - } - } - } - - internal void AddEditorRow(int index, GameObject groupObj) - { - var rowObj = UIFactory.CreateHorizontalGroup(groupObj, "EditorRow", false, true, true, true, 5, default, new Color(1, 1, 1, 0)); - - var label = UIFactory.CreateLabel(rowObj, "RowLabel", $"{StructInfo.FieldNames[index]}:", TextAnchor.MiddleRight, Color.cyan); - UIFactory.SetLayoutElement(label.gameObject, minWidth: 50, flexibleWidth: 0, minHeight: 25); - - var inputFieldObj = UIFactory.CreateInputField(rowObj, "InputField", "...", 14, 3, 1); - UIFactory.SetLayoutElement(inputFieldObj, minWidth: 120, minHeight: 25, flexibleWidth: 0); - - var inputField = inputFieldObj.GetComponent(); - m_inputs[index] = inputField; - - inputField.onValueChanged.AddListener((string val) => { Value = StructInfo.SetValue(ref this.Value, index, float.Parse(val)); }); - } - - #endregion - } -} diff --git a/src/UI/InteractiveValues/InteractiveValue.cs b/src/UI/InteractiveValues/InteractiveValue.cs index 660ee0d..343a9b7 100644 --- a/src/UI/InteractiveValues/InteractiveValue.cs +++ b/src/UI/InteractiveValues/InteractiveValue.cs @@ -49,8 +49,8 @@ namespace UnityExplorer.UI.InteractiveValues // check for unity struct types else if (typeof(Color).IsAssignableFrom(type)) return typeof(InteractiveColor); - else if (InteractiveUnityStruct.SupportsType(type)) - return typeof(InteractiveUnityStruct); + else if (InteractiveFloatStruct.IsTypeSupported(type)) + return typeof(InteractiveFloatStruct); // check Transform, force InteractiveValue so they dont become InteractiveEnumerables. else if (typeof(Transform).IsAssignableFrom(type)) return typeof(InteractiveValue); diff --git a/src/UnityExplorer.csproj b/src/UnityExplorer.csproj index d5168cb..c957bd5 100644 --- a/src/UnityExplorer.csproj +++ b/src/UnityExplorer.csproj @@ -273,7 +273,7 @@ - +