using System; using System.Globalization; using System.Linq; using UnityEngine; namespace NitroxClient.Debuggers.Drawer; // Reference https://gist.github.com/Seneral/31161381f993a4a06c59bf12576cabd8#file-rteditorgui-cs public static class NitroxGUILayout { private static int activeConvertibleField = -1; private static IConvertible activeConvertibleFieldLastValue = 0; private static string activeConvertibleFieldString = ""; private static readonly GUIStyle separatorStyle = new() { stretchWidth = true }; public static readonly GUIStyle DrawerLabel = new("options_label") { fixedHeight = 22, alignment = TextAnchor.LowerLeft }; public const float VALUE_WIDTH = 175; public const float DEFAULT_LABEL_WIDTH = 200; public const float DEFAULT_SPACE = 10; public static void Separator() { GUILayout.Box(GUIContent.none, separatorStyle, GUILayout.Height(5)); } public static int IntField(int value, float valueWidth = VALUE_WIDTH) => ConvertibleField(value, valueWidth).ToInt32(CultureInfo.CurrentCulture); public static float FloatField(float value, float valueWidth = VALUE_WIDTH) => ConvertibleField(value, valueWidth).ToSingle(CultureInfo.CurrentCulture); public static IConvertible ConvertibleField(IConvertible value, float valueWidth = VALUE_WIDTH) { int floatFieldID = GUIUtility.GetControlID("ConvertibleField".GetHashCode(), FocusType.Keyboard) + 1; if (floatFieldID == 0) { return value; } bool recorded = activeConvertibleField == floatFieldID; bool active = floatFieldID == GUIUtility.keyboardControl; if (active && recorded && !Equals(activeConvertibleFieldLastValue, value)) { // Value has been modified externally activeConvertibleFieldLastValue = value; activeConvertibleFieldString = value.ToString(CultureInfo.CurrentCulture); } // Get stored string for the text field if this one is recorded string str = recorded ? activeConvertibleFieldString : value.ToString(CultureInfo.CurrentCulture); string strValue = GUILayout.TextField(str, GUILayout.Width(valueWidth)); if (recorded) { activeConvertibleFieldString = strValue; } // Try Parse if value got changed. If the string could not be parsed, ignore it and keep last value bool parsed = true; if (string.IsNullOrEmpty(strValue)) { value = activeConvertibleFieldLastValue = 0; } else if (strValue != value.ToString(CultureInfo.CurrentCulture)) { parsed = TryParseIConvertible(value, strValue, out IConvertible newValue); if (parsed) { value = activeConvertibleFieldLastValue = newValue; } } switch (active) { case true when !recorded: // Gained focus this frame activeConvertibleField = floatFieldID; activeConvertibleFieldString = strValue; activeConvertibleFieldLastValue = value; break; case false when recorded: // Lost focus this frame { activeConvertibleField = -1; if (parsed) { break; } value = TryParseIConvertible(value, strValue, out IConvertible newValue) ? newValue : activeConvertibleFieldLastValue; break; } } return value; } private static bool TryParseIConvertible(IConvertible type, string inputString, out IConvertible newValue) { bool parsed; switch (type) { case short: parsed = short.TryParse(inputString, NumberStyles.Integer, CultureInfo.CurrentCulture, out short newShort); newValue = newShort; break; case ushort: parsed = ushort.TryParse(inputString, NumberStyles.Integer, CultureInfo.CurrentCulture, out ushort newUShort); newValue = newUShort; break; case int: parsed = int.TryParse(inputString, NumberStyles.Integer, CultureInfo.CurrentCulture, out int newInt); newValue = newInt; break; case uint _: parsed = uint.TryParse(inputString, NumberStyles.Integer, CultureInfo.CurrentCulture, out uint newUInt); newValue = newUInt; break; case long: parsed = long.TryParse(inputString, NumberStyles.Integer, CultureInfo.CurrentCulture, out long newLong); newValue = newLong; break; case ulong: parsed = ulong.TryParse(inputString, NumberStyles.Integer, CultureInfo.CurrentCulture, out ulong newULong); newValue = newULong; break; case float: parsed = float.TryParse(inputString, NumberStyles.Float, CultureInfo.CurrentCulture, out float newFloat); newValue = newFloat; break; case double: parsed = double.TryParse(inputString, NumberStyles.Float, CultureInfo.CurrentCulture, out double newDouble); newValue = newDouble; break; default: parsed = false; newValue = null; break; } return parsed; } public static int SliderField(int value, int minValue, int maxValue, float valueWidth = VALUE_WIDTH) => (int)SliderField((float)value, minValue, maxValue, valueWidth); public static float SliderField(float value, float minValue, float maxValue, float valueWidth = VALUE_WIDTH) { //TODO: Implement slider (if possible at all) return Math.Max(minValue, Math.Min(maxValue, FloatField(value, valueWidth))); } /// /// Displays an enum of an unknown type. /// /// The selected enum value. /// The button width /// The newly selected enum value. public static Enum EnumPopup(Enum selected, float buttonWidth = VALUE_WIDTH) { return EnumPopupInternal(selected, buttonWidth); } public static T EnumPopup(T selected, float buttonWidth = VALUE_WIDTH) where T : Enum { return (T)EnumPopupInternal(selected, buttonWidth); } /// /// Displays an enum of a known type. /// /// The selected enum value. /// The button width. /// The newly selected enum value. private static Enum EnumPopupInternal(Enum selected, float buttonWidth = VALUE_WIDTH) { Type enumType = selected.GetType(); string[] enumNames = Enum.GetNames(enumType); // Enums can be bit flags. If this is the case, we need to support toggling the bits if (enumType.CustomAttributes.Select(a => a.AttributeType).Contains(typeof(FlagsAttribute))) { bool IsFlagSet(T value, T flag) { long lValue = Convert.ToInt64(value); long lFlag = Convert.ToInt64(flag); return (lValue & lFlag) != 0; }; object SetFlags(Type type, object value, object flags, bool toggle) { long lValue = Convert.ToInt64(value); long lFlag = Convert.ToInt64(flags); if (toggle) { lValue |= lFlag; } else { lValue &= (~lFlag); } if (lFlag == 0) { lValue = 0; } return Enum.ToObject(type, lValue); }; Enum[] enumValues = Enum.GetValues(enumType).Cast().ToArray(); using (new GUILayout.VerticalScope()) { for (int i = 0; i < enumValues.Length; i++) { Enum enumValue = enumValues[i]; string enumName = enumNames[i]; bool isFlagSet = IsFlagSet(selected, enumValue); selected = (Enum) SetFlags(enumType, selected, enumValue, GUILayout.Toggle(isFlagSet, enumName, "Button", GUILayout.Width(buttonWidth))); } } } else { // Normal enum, only picks one value int selectedIndex = Array.IndexOf(enumNames, selected.ToString()); selectedIndex = GUILayout.SelectionGrid(selectedIndex, enumNames, 1, GUILayout.Width(buttonWidth)); return (Enum)Enum.Parse(enumType, enumNames[selectedIndex]); } return selected; } public static bool BoolField(bool value, float valueWidth = VALUE_WIDTH) => BoolFieldInternal(value, value.ToString(), valueWidth); public static bool BoolField(bool value, string name, float valueWidth = VALUE_WIDTH) => BoolFieldInternal(value, $"{name}: {value}", valueWidth); private static bool BoolFieldInternal(bool value, string buttonLabel, float valueWidth = VALUE_WIDTH) { if (GUILayout.Button(buttonLabel, GUILayout.Width(valueWidth))) { return !value; } return value; } public struct BackgroundColorScope : IDisposable { private bool disposed; private readonly Color previousColor; public BackgroundColorScope(Color newColor) { disposed = false; previousColor = GUI.color; GUI.backgroundColor = newColor; } public void Dispose() { if (disposed) { return; } disposed = true; GUI.backgroundColor = previousColor; } } }