using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using GUIEvent = UnityEngine.Event;
namespace Unity.VisualScripting
{
public static class LudiqGUI
{
#region Overrides
public static readonly OverrideStack<Color> color = new OverrideStack<Color>(
() => GUI.color,
(value) => GUI.color = value
);
public static readonly OverrideStack<Matrix4x4> matrix = new OverrideStack<Matrix4x4>(
() => GUI.matrix,
(value) => GUI.matrix = value
);
#endregion
#region Drawing
private static GUIStyle emptyRect;
public static void DrawEmptyRect(Rect position, Color color)
{
if (emptyRect == null)
{
emptyRect = new GUIStyle();
emptyRect.normal.background = ColorUtility.CreateBox($"{EmbeddedResourceProvider.VISUAL_SCRIPTING_PACKAGE}.emptyRect", ColorPalette.transparent, Color.white);
emptyRect.border = new RectOffset(1, 1, 1, 1);
}
if (e.type == EventType.Repaint)
{
using (LudiqGUI.color.Override(color))
{
emptyRect.Draw(position, false, false, false, false);
}
}
}
#endregion
private static GUIEvent e => GUIEvent.current;
public static void WindowHeader(string label, EditorTexture icon)
{
GUILayout.BeginVertical(LudiqStyles.windowHeaderBackground, GUILayout.ExpandWidth(true));
GUILayout.BeginHorizontal(GUILayout.ExpandWidth(true));
LudiqGUI.FlexibleSpace();
if (icon != null)
{
GUILayout.Box(new GUIContent(icon?[(int)LudiqStyles.windowHeaderIcon.fixedWidth]), LudiqStyles.windowHeaderIcon);
LudiqGUI.Space(LudiqStyles.spaceBetweenWindowHeaderIconAndTitle);
}
GUILayout.Label(label, LudiqStyles.windowHeaderTitle);
LudiqGUI.FlexibleSpace();
LudiqGUI.EndHorizontal();
LudiqGUI.EndVertical();
}
#region Inspectors & Editors
public static float GetInspectorHeight(Inspector parentInspector, Metadata metadata, float width, GUIContent label = null)
{
return metadata.Inspector().GetCachedHeight(width, label, parentInspector);
}
public static float GetInspectorAdaptiveWidth(Metadata metadata)
{
return metadata.Inspector().GetAdaptiveWidth();
}
public static void Inspector(Metadata metadata, Rect position, GUIContent label = null)
{
metadata.Inspector().Draw(position, label);
}
public static void InspectorLayout(Metadata metadata, GUIContent label = null, float scrollbarTrigger = 14, RectOffset offset = null)
{
metadata.Inspector().DrawLayout(label, scrollbarTrigger, offset);
}
public static float GetEditorHeight(Inspector parentInspector, Metadata metadata, float width)
{
return metadata.Editor().GetCachedHeight(width, GUIContent.none, parentInspector);
}
public static void Editor(Metadata metadata, Rect position)
{
metadata.Editor().Draw(position, GUIContent.none);
}
public static void EditorLayout(Metadata metadata)
{
metadata.Editor().DrawLayout(GUIContent.none);
}
#endregion
#region Loaders
public static readonly TextureResolution loaderResolution = new TextureResolution(loaderSize * loaderFrames, loaderSize);
public const int loaderSize = 24;
private const int loaderFrames = 12;
private const float loaderSpeed = 12; // FPS
private static EditorTexture temporaryLoader;
public static void Loader(Rect position)
{
EditorTexture loader;
if (PluginContainer.initialized)
{
loader = BoltCore.Resources.loader;
}
else
{
if (temporaryLoader == null && File.Exists(PluginPaths.resourcesBundle))
{
var assetBundleResourceProvider = new AssetBundleResourceProvider(AssetUtility.AssetBundleEditor);
temporaryLoader = EditorTexture.Load(assetBundleResourceProvider, "Loader.png", CreateTextureOptions.PixelPerfect, true);
}
loader = temporaryLoader;
}
if (loader != null)
{
var frame = (int)(EditorApplication.timeSinceStartup * loaderSpeed % loaderFrames);
var uv = new Rect((float)frame / loaderFrames, 0, 1f / loaderFrames, 1);
GUI.DrawTextureWithTexCoords(position, loader[loaderSize], uv);
}
}
public static void LoaderLayout()
{
Loader(GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Width(loaderSize), GUILayout.Height(loaderSize)));
}
public static void CenterLoader()
{
BeginVertical();
FlexibleSpace();
BeginHorizontal();
FlexibleSpace();
LoaderLayout();
FlexibleSpace();
EndHorizontal();
FlexibleSpace();
EndVertical();
}
#endregion
#region Fields
public static float GetTypeFieldHeight(GUIContent label, Type type)
{
return EditorGUIUtility.singleLineHeight;
}
public static float GetTypeFieldAdaptiveWidth(Type type, GUIContent nullLabel = null)
{
return Mathf.Max(18, EditorStyles.popup.CalcSize(GetTypeFieldPopupLabel(type)).x + 1);
}
private static GUIContent GetTypeFieldPopupLabel(Type type, GUIContent nullLabel = null)
{
GUIContent popupLabel;
if (type != null)
{
popupLabel = new GUIContent(type.DisplayName(), type.Icon()?[IconSize.Small]);
}
else
{
if (nullLabel == null)
{
nullLabel = new GUIContent("(No Type)");
}
popupLabel = nullLabel;
}
if (popupLabel.image != null)
{
popupLabel.text = " " + popupLabel.text;
}
return popupLabel;
}
public static Type TypeField(Rect position, GUIContent label, Type type, Func<IFuzzyOptionTree> getOptions, GUIContent nullLabel = null)
{
position = EditorGUI.PrefixLabel(position, label);
return (Type)FuzzyPopup
(
position,
getOptions,
type,
GetTypeFieldPopupLabel(type, nullLabel)
);
}
public static Vector2 CompactVector2Field(Rect position, GUIContent label, Vector2 value)
{
position = EditorGUI.PrefixLabel(position, label);
var xPosition = new Rect
(
position.x,
position.y,
(position.width / 2) - (LudiqStyles.compactHorizontalSpacing) / 1,
EditorGUIUtility.singleLineHeight
);
var yPosition = new Rect
(
xPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
(position.width / 2) - (LudiqStyles.compactHorizontalSpacing) / 1,
EditorGUIUtility.singleLineHeight
);
return new Vector2
(
DraggableFloatField(xPosition, value.x),
DraggableFloatField(yPosition, value.y)
);
}
public static Vector3 CompactVector3Field(Rect position, GUIContent label, Vector3 value)
{
position = EditorGUI.PrefixLabel(position, label);
var xPosition = new Rect
(
position.x,
position.y,
(position.width / 3) - (LudiqStyles.compactHorizontalSpacing) / 2,
EditorGUIUtility.singleLineHeight
);
var yPosition = new Rect
(
xPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
(position.width / 3) - (LudiqStyles.compactHorizontalSpacing) / 2,
EditorGUIUtility.singleLineHeight
);
var zPosition = new Rect
(
yPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
(position.width / 3) - (LudiqStyles.compactHorizontalSpacing) / 2,
EditorGUIUtility.singleLineHeight
);
return new Vector3
(
DraggableFloatField(xPosition, value.x),
DraggableFloatField(yPosition, value.y),
DraggableFloatField(zPosition, value.z)
);
}
public static Vector4 CompactVector4Field(Rect position, GUIContent label, Vector4 value)
{
position = EditorGUI.PrefixLabel(position, label);
var xPosition = new Rect
(
position.x,
position.y,
(position.width / 4) - (LudiqStyles.compactHorizontalSpacing) / 3,
EditorGUIUtility.singleLineHeight
);
var yPosition = new Rect
(
xPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
(position.width / 4) - (LudiqStyles.compactHorizontalSpacing) / 3,
EditorGUIUtility.singleLineHeight
);
var zPosition = new Rect
(
yPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
(position.width / 4) - (LudiqStyles.compactHorizontalSpacing) / 3,
EditorGUIUtility.singleLineHeight
);
var wPosition = new Rect
(
zPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
(position.width / 4) - (LudiqStyles.compactHorizontalSpacing) / 3,
EditorGUIUtility.singleLineHeight
);
return new Vector4
(
DraggableFloatField(xPosition, value.x),
DraggableFloatField(yPosition, value.y),
DraggableFloatField(zPosition, value.z),
DraggableFloatField(wPosition, value.w)
);
}
public static Vector2 AdaptiveVector2Field(Rect position, GUIContent label, Vector2 value)
{
position = EditorGUI.PrefixLabel(position, label);
var xPosition = new Rect
(
position.x,
position.y,
GetTextFieldAdaptiveWidth(value.x),
EditorGUIUtility.singleLineHeight
);
var yPosition = new Rect
(
xPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
GetTextFieldAdaptiveWidth(value.y),
EditorGUIUtility.singleLineHeight
);
return new Vector2
(
DraggableFloatField(xPosition, value.x),
DraggableFloatField(yPosition, value.y)
);
}
public static Vector3 AdaptiveVector3Field(Rect position, GUIContent label, Vector3 value)
{
position = EditorGUI.PrefixLabel(position, label);
var xPosition = new Rect
(
position.x,
position.y,
GetTextFieldAdaptiveWidth(value.x),
EditorGUIUtility.singleLineHeight
);
var yPosition = new Rect
(
xPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
GetTextFieldAdaptiveWidth(value.y),
EditorGUIUtility.singleLineHeight
);
var zPosition = new Rect
(
yPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
GetTextFieldAdaptiveWidth(value.z),
EditorGUIUtility.singleLineHeight
);
return new Vector3
(
DraggableFloatField(xPosition, value.x),
DraggableFloatField(yPosition, value.y),
DraggableFloatField(zPosition, value.z)
);
}
public static Vector4 AdaptiveVector4Field(Rect position, GUIContent label, Vector4 value)
{
position = EditorGUI.PrefixLabel(position, label);
var xPosition = new Rect
(
position.x,
position.y,
GetTextFieldAdaptiveWidth(value.x),
EditorGUIUtility.singleLineHeight
);
var yPosition = new Rect
(
xPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
GetTextFieldAdaptiveWidth(value.y),
EditorGUIUtility.singleLineHeight
);
var zPosition = new Rect
(
yPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
GetTextFieldAdaptiveWidth(value.z),
EditorGUIUtility.singleLineHeight
);
var wPosition = new Rect
(
zPosition.xMax + LudiqStyles.compactHorizontalSpacing,
position.y,
GetTextFieldAdaptiveWidth(value.w),
EditorGUIUtility.singleLineHeight
);
return new Vector4
(
DraggableFloatField(xPosition, value.x),
DraggableFloatField(yPosition, value.y),
DraggableFloatField(zPosition, value.z),
DraggableFloatField(wPosition, value.w)
);
}
public static GUIContent GetEnumPopupContent(Enum value)
{
Ensure.That(nameof(value)).IsNotNull(value);
if (EditorGUI.showMixedValue)
{
return new GUIContent("Mixed ...");
}
var enumType = value.GetType();
if (enumType.HasAttribute<FlagsAttribute>())
{
var mask = Convert.ToInt64(value);
if (mask == 0)
{
return new GUIContent("None");
}
else if (mask == ~0)
{
return new GUIContent("Everything");
}
var flags = Enum.GetValues(enumType).Cast<Enum>().ToArray();
var activeFlagsCount = 0;
foreach (var flag in flags)
{
if (value.HasFlag(flag))
{
activeFlagsCount++;
}
}
if (activeFlagsCount == 0)
{
return new GUIContent("None");
}
else if (activeFlagsCount == 1)
{
return new GUIContent(value.ToString().Prettify());
}
else if (activeFlagsCount == flags.Length)
{
return new GUIContent("Everything");
}
else
{
return new GUIContent("Mixed ...");
}
}
else
{
return new GUIContent(value.ToString().Prettify());
}
}
public static int Spinner(Rect position, bool upEnabled = true, bool downEnabled = true)
{
var upPosition = new Rect
(
position.x,
position.y,
position.width,
position.height / 2
);
var downPosition = new Rect
(
position.x,
position.y + (position.height / 2),
position.width,
position.height / 2
);
EditorGUI.BeginDisabledGroup(!upEnabled);
if (GUI.Button(upPosition, GUIContent.none, LudiqStyles.spinnerButton))
{
return 1;
}
EditorGUI.EndDisabledGroup();
EditorGUI.BeginDisabledGroup(!downEnabled);
if (GUI.Button(downPosition, GUIContent.none, LudiqStyles.spinnerButton))
{
return -1;
}
EditorGUI.EndDisabledGroup();
var arrow = LudiqStyles.spinnerDownArrow;
var upArrowPosition = new Rect
(
upPosition.x + ((upPosition.width - arrow.width) / 2),
upPosition.y + ((upPosition.height - arrow.height) / 2) + arrow.height - 1,
arrow.width,
-arrow.height
);
var downArrowPosition = new Rect
(
downPosition.x + ((downPosition.width - arrow.width) / 2),
downPosition.y + ((downPosition.height - arrow.height) / 2) + 1,
arrow.width,
arrow.height
);
using (color.Override(upEnabled ? GUI.color : GUI.color.WithAlpha(0.3f)))
{
GUI.DrawTexture(upArrowPosition, arrow);
}
using (color.Override(downEnabled ? GUI.color : GUI.color.WithAlpha(0.3f)))
{
GUI.DrawTexture(downArrowPosition, arrow);
}
return 0;
}
#endregion
#region Number Dragging
// Lots of re-implementation from internal EditorGUI methods to allow us custom control trigger
private static double CalculateDragSensitivityContinuous(double value)
{
if (Double.IsInfinity(value) || Double.IsNaN(value))
return 0.0;
return Math.Max(1.0, Math.Pow(Math.Abs(value), 0.5)) * 0.0299999993294477;
}
private static long CalculateDragSensitivityDiscrete(long value)
{
return (long)Math.Max(1.0, Math.Pow(Math.Abs((double)value), 0.5) * 0.0299999993294477);
}
private static NumberDragState numberDragState = NumberDragState.NotDragging;
private static double numberDragStartValueContinuous;
private static long numberDragStartValueDiscrete;
private static Vector2 numberDragStartPosition;
private static double numberDragSensitivity;
private const float numberDragDeadZone = 16;
private static readonly int numberDragControlIDHint = "DraggableFieldOverlay".GetHashCode();
private enum NumberDragState
{
NotDragging,
RequestedDragging,
Dragging
}
public static long DraggableLongField(Rect position, long value, GUIContent label = null)
{
var controlId = GUIUtility.GetControlID(numberDragControlIDHint, FocusType.Passive, position);
if (e.shift)
{
value = DragNumber(position, true, controlId, value);
}
return label != null ? EditorGUI.LongField(position, label, value) : EditorGUI.LongField(position, value);
}
public static float DraggableFloatField(Rect position, float value, GUIContent label = null)
{
var controlId = GUIUtility.GetControlID(numberDragControlIDHint, FocusType.Passive, position);
if (e.shift)
{
value = DragNumber(position, true, controlId, value);
}
return label != null ? EditorGUI.FloatField(position, label, value) : EditorGUI.FloatField(position, value);
}
public static int DraggableIntField(Rect position, int value, GUIContent label = null)
{
var controlId = GUIUtility.GetControlID(numberDragControlIDHint, FocusType.Passive, position);
if (e.shift)
{
value = DragNumber(position, true, controlId, value);
}
return label != null ? EditorGUI.IntField(position, label, value) : EditorGUI.IntField(position, value);
}
public static long DragNumber(Rect hotZone, bool deadZone, int controlId, long value)
{
double continuousValue = value;
long discreteValue = 0;
DragNumber(hotZone, deadZone, controlId, true, ref continuousValue, ref discreteValue);
return (long)continuousValue;
}
public static float DragNumber(Rect hotZone, bool deadZone, int controlId, float value)
{
double continuousValue = value;
long discreteValue = 0;
DragNumber(hotZone, deadZone, controlId, true, ref continuousValue, ref discreteValue);
return (float)continuousValue;
}
public static int DragNumber(Rect hotZone, bool deadZone, int controlId, int value)
{
double continuousValue = 0;
long discreteValue = value;
DragNumber(hotZone, deadZone, controlId, false, ref continuousValue, ref discreteValue);
return (int)discreteValue;
}
private static void DragNumber(Rect hotZone, bool deadZone, int controlId, bool isContinuous, ref double continuousValue, ref long discreteValue)
{
var e = GUIEvent.current;
switch (e.GetTypeForControl(controlId))
{
case EventType.MouseDown:
{
if (!hotZone.Contains(e.mousePosition) || e.button != (int)MouseButton.Left)
{
break;
}
EditorGUIUtility.editingTextField = false;
GUIUtility.hotControl = controlId;
GUIUtility.keyboardControl = controlId;
if (deadZone)
{
numberDragState = NumberDragState.RequestedDragging;
}
else
{
numberDragState = NumberDragState.Dragging;
}
numberDragStartValueContinuous = continuousValue;
numberDragStartValueDiscrete = discreteValue;
numberDragStartPosition = e.mousePosition;
if (isContinuous)
{
numberDragSensitivity = CalculateDragSensitivityContinuous(continuousValue);
}
else
{
numberDragSensitivity = CalculateDragSensitivityDiscrete(discreteValue);
}
e.Use();
EditorGUIUtility.SetWantsMouseJumping(1);
break;
}
case EventType.MouseUp:
{
if (GUIUtility.hotControl != controlId || numberDragState == NumberDragState.NotDragging)
{
break;
}
GUIUtility.hotControl = 0;
numberDragState = NumberDragState.NotDragging;
e.Use();
EditorGUIUtility.SetWantsMouseJumping(0);
break;
}
case EventType.MouseDrag:
{
if (GUIUtility.hotControl != controlId)
{
break;
}
switch (numberDragState)
{
case NumberDragState.RequestedDragging:
{
if ((e.mousePosition - numberDragStartPosition).sqrMagnitude > numberDragDeadZone)
{
numberDragState = NumberDragState.Dragging;
GUIUtility.keyboardControl = controlId;
}
e.Use();
break;
}
case NumberDragState.Dragging:
{
if (isContinuous)
{
continuousValue = continuousValue + HandleUtility.niceMouseDelta * numberDragSensitivity;
continuousValue = RoundBasedOnMinimumDifference(continuousValue, numberDragSensitivity);
}
else
{
discreteValue = discreteValue + (long)Math.Round(HandleUtility.niceMouseDelta * numberDragSensitivity);
}
GUI.changed = true;
e.Use();
break;
}
}
break;
}
case EventType.KeyDown:
{
if (GUIUtility.hotControl != controlId || e.keyCode != KeyCode.Escape || numberDragState == NumberDragState.NotDragging)
{
break;
}
continuousValue = numberDragStartValueContinuous;
discreteValue = numberDragStartValueDiscrete;
GUI.changed = true;
GUIUtility.hotControl = 0;
e.Use();
break;
}
case EventType.Repaint:
{
EditorGUIUtility.AddCursorRect(hotZone, MouseCursor.SlideArrow);
break;
}
}
}
private static double DiscardLeastSignificantDecimal(double v)
{
int digits = Math.Max(0, (int)(5d - Math.Log10(Math.Abs(v))));
try
{
return Math.Round(v, digits);
}
catch (ArgumentOutOfRangeException)
{
return 0d;
}
}
private static int GetNumberOfDecimalsForMinimumDifference(double minDifference)
{
return (int)Math.Max(0d, -Math.Floor(Math.Log10(Math.Abs(minDifference))));
}
private static double RoundBasedOnMinimumDifference(double valueToRound, double minDifference)
{
if (minDifference == 0d)
{
return DiscardLeastSignificantDecimal(valueToRound);
}
return Math.Round(valueToRound, GetNumberOfDecimalsForMinimumDifference(minDifference), MidpointRounding.AwayFromZero);
}
#endregion
#region Headers
public delegate float GetHeaderTitleHeightDelegate(float innerWidth);
public delegate float GetHeaderSummaryHeightDelegate(float innerWidth);
public delegate void OnHeaderTitleGUIDelegate(Rect titlePosition);
public delegate void OnHeaderSummaryGUIDelegate(Rect summaryPosition);
public static float GetHeaderHeight
(
GetHeaderTitleHeightDelegate getTitleHeight,
GetHeaderSummaryHeightDelegate getSummaryHeight,
EditorTexture icon,
float totalWidth,
bool bottomMargin = true,
float spaceBetweenTitleAndSummary = 0
)
{
var innerWidth = GetHeaderInnerWidth(totalWidth, icon);
var height = 0f;
height += LudiqStyles.headerBackground.padding.top;
var innerHeight = 0f;
innerHeight += getTitleHeight(innerWidth);
var summaryHeight = getSummaryHeight(innerWidth);
if (summaryHeight > 0)
{
innerHeight += spaceBetweenTitleAndSummary;
innerHeight += summaryHeight;
}
if (icon != null)
{
innerHeight = Mathf.Max(innerHeight, LudiqStyles.headerIcon.fixedHeight + LudiqStyles.headerIcon.margin.top);
}
height += innerHeight;
height += LudiqStyles.headerBackground.padding.bottom;
if (bottomMargin)
{
height += LudiqStyles.headerBackground.margin.bottom;
}
return height;
}
public static void OnHeaderGUI
(
GetHeaderTitleHeightDelegate getTitleHeight,
GetHeaderSummaryHeightDelegate getSummaryHeight,
OnHeaderTitleGUIDelegate onTitleGUI,
OnHeaderSummaryGUIDelegate onSummaryGui,
EditorTexture icon,
Rect position,
ref float y,
bool bottomMargin = true,
float spaceBetweenTitleAndSummary = 0
)
{
var innerWidth = GetHeaderInnerWidth(position.width, icon);
var x = position.x;
var headerPosition = new Rect
(
x,
y,
position.width,
GetHeaderHeight(getTitleHeight, getSummaryHeight, icon, position.width, false)
);
if (e.type == EventType.Repaint)
{
LudiqStyles.headerBackground.Draw(headerPosition, false, false, false, false);
}
x += LudiqStyles.headerBackground.padding.left;
if (icon != null)
{
var iconPosition = new Rect
(
x,
y + LudiqStyles.headerBackground.padding.top + LudiqStyles.headerIcon.margin.top,
LudiqStyles.headerIcon.fixedWidth,
LudiqStyles.headerIcon.fixedHeight
);
OnHeaderIconGUI(icon, iconPosition);
x += iconPosition.width + LudiqStyles.headerIcon.margin.right;
}
var titlePosition = new Rect
(
x,
y + LudiqStyles.headerBackground.padding.top,
innerWidth,
getTitleHeight(innerWidth)
);
onTitleGUI(titlePosition);
var summaryHeight = getSummaryHeight(innerWidth);
if (summaryHeight > 0)
{
var summaryPosition = new Rect
(
x,
titlePosition.yMax + spaceBetweenTitleAndSummary,
innerWidth,
summaryHeight
);
onSummaryGui(summaryPosition);
}
y = headerPosition.yMax;
if (bottomMargin)
{
y += LudiqStyles.headerBackground.margin.bottom;
}
}
private static float GetHeaderInnerWidth(float totalWidth, EditorTexture icon)
{
var width = totalWidth;
width -= LudiqStyles.headerBackground.padding.left;
width -= LudiqStyles.headerBackground.padding.right;
if (icon != null)
{
width -= LudiqStyles.headerIcon.fixedWidth;
width -= LudiqStyles.headerIcon.margin.right;
}
return width;
}
private static void OnHeaderIconGUI(EditorTexture icon, Rect iconPosition)
{
if (icon != null && icon[IconSize.Medium])
{
GUI.DrawTexture(iconPosition, icon?[IconSize.Medium]);
}
}
#region Static
public static float GetHeaderHeight(GUIContent header, float totalWidth, bool bottomMargin = true)
{
var title = new GUIContent(header.text);
var summary = new GUIContent(header.tooltip);
var icon = EditorTexture.Single(header.image);
return GetHeaderHeight
(
innerWidth => GetHeaderTitleHeight(title, innerWidth),
innerWidth => GetHeaderSummaryHeight(summary, innerWidth),
icon,
totalWidth,
bottomMargin,
0
);
}
public static void OnHeaderGUI(GUIContent header, Rect position, ref float y, bool bottomMargin = true)
{
var title = new GUIContent(header.text);
var summary = new GUIContent(header.tooltip);
var icon = EditorTexture.Single(header.image);
OnHeaderGUI
(
innerWidth => GetHeaderTitleHeight(title, innerWidth),
innerWidth => GetHeaderSummaryHeight(summary, innerWidth),
titlePosition => OnHeaderTitleGUI(title, titlePosition),
summaryPosition => OnHeaderSummaryGUI(summary, summaryPosition),
icon,
position,
ref y,
bottomMargin,
0
);
}
private static float GetHeaderTitleHeight(GUIContent title, float width)
{
return LudiqStyles.headerTitle.CalcHeight(title, width);
}
private static float GetHeaderSummaryHeight(GUIContent summary, float width)
{
if (StringUtility.IsNullOrWhiteSpace(summary.text))
{
return 0;
}
return LudiqStyles.headerSummary.CalcHeight(summary, width);
}
private static void OnHeaderTitleGUI(GUIContent title, Rect titlePosition)
{
EditorGUI.LabelField(titlePosition, title, LudiqStyles.headerTitle);
}
private static void OnHeaderSummaryGUI(GUIContent summary, Rect summaryPosition)
{
EditorGUI.LabelField(summaryPosition, summary, LudiqStyles.headerSummary);
}
#endregion
#region Editable
public static float GetHeaderHeight(Inspector parentInspector, Metadata titleMetadata, Metadata summaryMetadata, EditorTexture icon, float totalWidth, bool bottomMargin = true)
{
return GetHeaderHeight
(
innerWidth => GetHeaderTitleHeight(parentInspector, titleMetadata, innerWidth),
innerWidth => GetHeaderSummaryHeight(parentInspector, summaryMetadata, innerWidth),
icon,
totalWidth,
bottomMargin,
EditorGUIUtility.standardVerticalSpacing
);
}
public static void OnHeaderGUI(Metadata titleMetadata, Metadata summaryMetadata, EditorTexture icon, Rect position, ref float y, bool bottomMargin = true)
{
OnHeaderGUI
(
innerWidth => GetHeaderTitleHeight(null, titleMetadata, innerWidth),
innerWidth => GetHeaderSummaryHeight(null, summaryMetadata, innerWidth),
titlePosition => OnHeaderTitleGUI(titleMetadata, titlePosition),
summaryPosition => OnHeaderSummaryGUI(summaryMetadata, summaryPosition),
icon,
position,
ref y,
bottomMargin,
EditorGUIUtility.standardVerticalSpacing
);
}
private static float GetHeaderTitleHeight(Inspector parentInspector, Metadata titleMetadata, float width)
{
return LudiqStyles.headerTitleField.fixedHeight;
}
private static float GetHeaderSummaryHeight(Inspector parentInspector, Metadata summaryMetadata, float width)
{
var attribute = summaryMetadata.GetAttribute<InspectorTextAreaAttribute>();
var height = LudiqStyles.textAreaWordWrapped.CalcHeight(new GUIContent((string)summaryMetadata.value), width);
if (attribute.hasMinLines)
{
var minHeight = EditorStyles.textArea.lineHeight * attribute.minLines + EditorStyles.textArea.padding.top + EditorStyles.textArea.padding.bottom;
height = Mathf.Max(height, minHeight);
}
if (attribute.hasMaxLines)
{
var maxHeight = EditorStyles.textArea.lineHeight * attribute.maxLines + EditorStyles.textArea.padding.top + EditorStyles.textArea.padding.bottom;
height = Mathf.Min(height, maxHeight);
}
return height;
}
private static void OnHeaderTitleGUI(Metadata titleMetadata, Rect titlePosition)
{
VisualScripting.Inspector.BeginLabeledBlock(titleMetadata, titlePosition, GUIContent.none);
var hidable = !StringUtility.IsNullOrWhiteSpace((string)titleMetadata.value);
var newTitle = EditorGUI.TextField(titlePosition, (string)titleMetadata.value, hidable ? LudiqStyles.headerTitleFieldHidable : LudiqStyles.headerTitleField);
if (VisualScripting.Inspector.EndBlock(titleMetadata))
{
titleMetadata.RecordUndo();
titleMetadata.value = newTitle;
}
if (String.IsNullOrEmpty((string)titleMetadata.value))
{
GUI.Label(titlePosition, $"({titleMetadata.label.text})", LudiqStyles.headerTitlePlaceholder);
}
}
private static void OnHeaderSummaryGUI(Metadata summaryMetadata, Rect summaryPosition)
{
VisualScripting.Inspector.BeginLabeledBlock(summaryMetadata, summaryPosition, GUIContent.none);
var hidable = !StringUtility.IsNullOrWhiteSpace((string)summaryMetadata.value);
var newTitle = EditorGUI.TextArea(summaryPosition, (string)summaryMetadata.value, hidable ? LudiqStyles.headerSummaryFieldHidable : LudiqStyles.headerSummaryField);
if (VisualScripting.Inspector.EndBlock(summaryMetadata))
{
summaryMetadata.RecordUndo();
summaryMetadata.value = newTitle;
}
if (String.IsNullOrEmpty((string)summaryMetadata.value))
{
GUI.Label(summaryPosition, $"({summaryMetadata.label.text})", LudiqStyles.headerSummaryPlaceholder);
}
}
#endregion
#endregion
#region Version Mismatch
private const string VersionMismatchMessage = "Inspectors are disabled when plugin versions mismatch to prevent data corruption. ";
public static float GetVersionMismatchShieldHeight(float width)
{
var height = 0f;
height += LudiqGUIUtility.GetHelpBoxHeight(VersionMismatchMessage, MessageType.Warning, width);
height += EditorGUIUtility.standardVerticalSpacing;
height += EditorGUIUtility.singleLineHeight;
return height;
}
public static void VersionMismatchShield(Rect position)
{
var warningPosition = new Rect
(
position.x,
position.y,
position.width,
LudiqGUIUtility.GetHelpBoxHeight(VersionMismatchMessage, MessageType.Warning, position.width)
);
var buttonPosition = new Rect
(
position.x,
warningPosition.yMax + EditorGUIUtility.standardVerticalSpacing,
position.width,
EditorGUIUtility.singleLineHeight
);
EditorGUI.HelpBox(warningPosition, VersionMismatchMessage, MessageType.Warning);
}
public static void VersionMismatchShieldLayout()
{
LudiqGUI.BeginVertical();
EditorGUILayout.HelpBox(VersionMismatchMessage, MessageType.Warning);
LudiqGUI.EndVertical();
}
#endregion
#region Lists
private static float CalculateListWidth(IList<ListOption> options)
{
var width = 0f;
foreach (var option in options)
{
width = Mathf.Max(width, LudiqStyles.listRow.CalcSize(option.label).x);
}
return width + LudiqGUIUtility.scrollBarWidth;
}
public static Vector2 List(Vector2 scroll, IList<ListOption> options, object selected, Action<object> selectionChanged)
{
var selectedIndex = options.IndexOf(options.FirstOrDefault(o => Equals(o.value, selected)));
if (e.type == EventType.KeyDown)
{
if (e.keyCode == KeyCode.DownArrow)
{
selectionChanged(options[Mathf.Min(options.Count - 1, selectedIndex + 1)].value);
e.Use();
}
else if (e.keyCode == KeyCode.UpArrow)
{
selectionChanged(options[Mathf.Max(0, selectedIndex - 1)].value);
e.Use();
}
}
var newScroll = EditorGUILayout.BeginScrollView(scroll, LudiqStyles.listBackground, GUILayout.Width(CalculateListWidth(options)));
LudiqGUI.BeginVertical();
foreach (var option in options)
{
var wasSelected = Equals(option.value, selected);
var isSelected = ListOption(option.label, wasSelected);
if (!wasSelected && isSelected)
{
selectionChanged(option.value);
}
}
LudiqGUI.EndVertical();
GUILayout.EndScrollView();
return newScroll;
}
private static bool ListOption(GUIContent label, bool selected)
{
if (!String.IsNullOrEmpty(label.tooltip))
{
label = new GUIContent($"{label.text}\n<size=10>{label.tooltip}</size>", label.image);
}
return GUILayout.Toggle(selected, label, LudiqStyles.listRow, GUILayout.ExpandWidth(true)) && !selected;
}
#endregion
#region Standard Dropdowns
public static void Dropdown
(
Vector2 position,
Action<object> callback,
IEnumerable<DropdownOption> options,
object selected
)
{
var hasMultipleDifferentValues = EditorGUI.showMixedValue;
ICollection<DropdownOption> optionsCache = null;
bool hasOptions;
if (options != null)
{
optionsCache = options.AsReadOnlyCollection();
hasOptions = optionsCache.Count > 0;
}
else
{
hasOptions = false;
}
var menu = new GenericMenu();
GenericMenu.MenuFunction2 menuCallback = (o) =>
{
try
{
callback(o);
}
catch (ExitGUIException) { }
};
if (hasOptions)
{
var wasSeparator = false;
foreach (var option in optionsCache)
{
var isSeparator = option is DropdownSeparator;
if (isSeparator)
{
if (!wasSeparator)
{
menu.AddSeparator(((DropdownSeparator)option).path);
}
}
else
{
var on = !hasMultipleDifferentValues && OptionValuesEqual(selected, option.value);
menu.AddItem(new GUIContent(option.label), @on, menuCallback, option.value);
}
wasSeparator = isSeparator;
}
}
using (LudiqGUIUtility.fixedClip)
{
menu.DropDown(new Rect(position, Vector2.zero));
}
}
public static void Dropdown
(
Vector2 position,
Action<HashSet<object>> callback,
IEnumerable<DropdownOption> options,
HashSet<object> selected,
bool showNothingEverything = true
)
{
var hasMultipleDifferentValues = EditorGUI.showMixedValue;
ICollection<DropdownOption> optionsCache = null;
bool hasOptions;
if (options != null)
{
optionsCache = options.AsReadOnlyCollection();
hasOptions = optionsCache.Count > 0;
}
else
{
hasOptions = false;
}
var selectedCopy = selected.ToHashSet();
// Remove options outside range
selectedCopy.RemoveWhere(so => !optionsCache.Any(o => OptionValuesEqual(o.value, so)));
var menu = new GenericMenu();
// The callback when a normal option has been selected
GenericMenu.MenuFunction2 switchCallback = (o) =>
{
var switchValue = o;
if (selectedCopy.Contains(switchValue))
{
selectedCopy.Remove(switchValue);
}
else
{
selectedCopy.Add(switchValue);
}
try
{
callback(selectedCopy);
}
catch (ExitGUIException) { }
};
// The callback when the special "Nothing" option has been selected
GenericMenu.MenuFunction nothingCallback = () => { callback(new HashSet<object>()); };
// The callback when the special "Everything" option has been selected
GenericMenu.MenuFunction everythingCallback = () => { callback(optionsCache.Select((o) => o.value).ToHashSet()); };
// Add the special "Nothing" / "Everything" options
if (showNothingEverything)
{
menu.AddItem
(
new GUIContent("Nothing"),
!hasMultipleDifferentValues && selectedCopy.Count == 0,
nothingCallback
);
if (hasOptions)
{
menu.AddItem
(
new GUIContent("Everything"),
!hasMultipleDifferentValues && selectedCopy.Count == optionsCache.Count && selectedCopy.OrderBy(t => t).SequenceEqual(optionsCache.Select(o => o.value).OrderBy(t => t)),
everythingCallback
);
}
}
// Add a separator (not in Unity default, but pretty)
if (showNothingEverything && hasOptions)
{
menu.AddSeparator(String.Empty);
}
// Add the normal options
if (hasOptions)
{
foreach (var option in optionsCache)
{
menu.AddItem
(
new GUIContent(option.label),
!hasMultipleDifferentValues && (selectedCopy.Any(selectedValue => OptionValuesEqual(selectedValue, option.value))),
switchCallback,
option.value
);
}
}
// Show the dropdown
using (LudiqGUIUtility.fixedClip)
{
menu.DropDown(new Rect(position, Vector2.zero));
}
}
#endregion
#region Standard Popups
public static object Popup
(
Rect position,
Func<IEnumerable<DropdownOption>> getOptions,
object selected,
GUIContent label = null,
GUIStyle style = null
)
{
return ImmediatePopup
(
position,
label ?? DefaultPopupLabel(selected),
style,
selected,
() => Dropdown
(
new Vector2(position.xMin, position.yMax),
UpdateImmediatePopupValue,
getOptions(),
selected
)
);
}
public static HashSet<object> Popup
(
Rect position,
Func<IEnumerable<DropdownOption>> getOptions,
HashSet<object> selected,
bool showNothingEverything = true,
GUIContent label = null,
GUIStyle style = null
)
{
return ImmediatePopup
(
position,
label ?? DefaultPopupLabel(selected, getOptions().Count()),
style,
selected,
() => Dropdown
(
new Vector2(position.xMin, position.yMax),
UpdateImmediatePopupValue,
getOptions(),
selected,
showNothingEverything
)
);
}
#endregion
#region Fuzzy Dropdowns
public static void FuzzyDropdown
(
Rect activatorPosition,
IFuzzyOptionTree optionTree,
object selected,
Action<object> callback
)
{
optionTree.selected.Clear();
optionTree.selected.Add(selected);
FuzzyWindow.Show(activatorPosition, optionTree, (option) =>
{
callback(option.value);
FuzzyWindow.instance.Close();
InternalEditorUtility.RepaintAllViews();
});
}
public static void FuzzyDropdown
(
Rect activatorPosition,
IFuzzyOptionTree optionTree,
HashSet<object> selected,
Action<HashSet<object>> callback
)
{
optionTree.selected.Clear();
optionTree.selected.AddRange(selected);
FuzzyWindow.Show(activatorPosition, optionTree, (option) =>
{
callback(optionTree.selected.ToHashSet());
FuzzyWindow.instance.Close();
InternalEditorUtility.RepaintAllViews();
});
}
#endregion
#region Fuzzy Popups
public static object FuzzyPopup
(
Rect position,
Func<IFuzzyOptionTree> getProvider,
object selected,
GUIContent label = null,
GUIStyle style = null
)
{
Ensure.That(nameof(getProvider)).IsNotNull(getProvider);
return ImmediatePopup
(
position,
label ?? DefaultPopupLabel(selected),
style,
selected,
() => FuzzyDropdown
(
position,
getProvider(),
selected,
UpdateImmediatePopupValue
)
);
}
public static HashSet<object> FuzzyPopup
(
Rect position,
Func<IFuzzyOptionTree> getProvider,
HashSet<object> selected,
bool showNothingEverything = true,
GUIContent label = null,
GUIStyle style = null
)
{
Ensure.That(nameof(getProvider)).IsNotNull(getProvider);
return ImmediatePopup
(
position,
label ?? DefaultPopupLabel(getProvider().selected),
style,
selected,
() => FuzzyDropdown
(
position,
getProvider(),
selected,
UpdateImmediatePopupValues
)
);
}
#endregion
#region Immediate State Handling
private static int activeActivatorControlID = -1;
private static bool activeDropdownChanged;
private static object activeDropdownValue;
private static HashSet<object> activeDropdownValues;
public static void UpdateImmediatePopupValue(object value)
{
activeDropdownValue = value;
activeDropdownChanged = true;
}
public static void UpdateImmediatePopupValues(HashSet<object> value)
{
activeDropdownValues = value;
activeDropdownChanged = true;
}
private static bool PopupActivatorRaw
(
int controlID,
bool activated,
Action dropdown
)
{
if (activated)
{
// Assign the active control ID
activeActivatorControlID = controlID;
// Display the dropdown
dropdown();
}
if (controlID == activeActivatorControlID && activeDropdownChanged)
{
GUI.changed = true;
activeActivatorControlID = -1;
activeDropdownChanged = false;
return true;
}
else
{
return false;
}
}
public static object ImmediatePopup
(
int controlID,
bool activated,
object selected,
Action dropdown
)
{
if (PopupActivatorRaw(controlID, activated, dropdown))
{
return activeDropdownValue;
}
else
{
return selected;
}
}
public static HashSet<object> ImmediatePopup
(
int controlID,
bool activated,
HashSet<object> selected,
Action dropdown
)
{
if (PopupActivatorRaw(controlID, activated, dropdown))
{
return activeDropdownValues;
}
else
{
return selected;
}
}
private static readonly int PopupHash = "LudiqPopup".GetHashCode();
private static bool PopupActivator
(
Rect position,
GUIContent label,
GUIStyle style,
Action dropdown
)
{
if (style == null)
{
style = EditorStyles.popup;
}
style = LudiqGUIUtility.BoldedStyle(style);
// Render a button and get its control ID
// Note: I'm having a really hard time ensuring control ID
// continuity across immediate mode GUI. Internally, Unity
// simply uses a static hash for its popups, but that doesn't
// seem to be reliable. Using the internal s_LastControlID doesn't
// seem to be constant across OnGUI calls either. Maybe it's because
// I open another window in the mean time. To mitigate the effect,
// we're using the current inspector block metadata to strenghten our
// hash.
var activatorControlID = GUIUtility.GetControlID(HashUtility.GetHashCode(PopupHash, VisualScripting.Inspector.currentBlock.metadata), FocusType.Keyboard, position);
var activatorClicked = GUI.Button(position, label, style);
//var activatorControlID = LudiqGUIUtility.GetLastControlID();
if (activatorClicked)
{
// Cancel button click
GUI.changed = false;
}
return PopupActivatorRaw(activatorControlID, activatorClicked, dropdown);
}
private static object ImmediatePopup
(
Rect position,
GUIContent label,
GUIStyle style,
object selected,
Action dropdown
)
{
if (PopupActivator(position, label, style, dropdown))
{
return activeDropdownValue;
}
else
{
return selected;
}
}
private static HashSet<object> ImmediatePopup
(
Rect position,
GUIContent label,
GUIStyle style,
HashSet<object> selected,
Action dropdown
)
{
if (PopupActivator(position, label, style, dropdown))
{
return activeDropdownValues;
}
else
{
return selected;
}
}
#endregion
#region Popup Utility
private static bool OptionValuesEqual(object a, object b)
{
return Equals(a, b);
}
private static GUIContent DefaultPopupLabel(object selectedValue)
{
string text;
if (EditorGUI.showMixedValue)
{
text = "\u2014"; // Em Dash
}
else if (selectedValue != null)
{
text = selectedValue.ToString();
}
else
{
text = String.Empty;
}
return new GUIContent(text);
}
private static GUIContent DefaultPopupLabel(HashSet<object> selectedValues)
{
string text;
if (EditorGUI.showMixedValue)
{
text = "\u2014"; // Em Dash
}
else
{
if (selectedValues.Count == 0)
{
text = "Nothing";
}
else if (selectedValues.Count == 1)
{
text = selectedValues.First().ToString();
}
else
{
text = "(Multiple)";
}
}
return new GUIContent(text);
}
private static GUIContent DefaultPopupLabel(HashSet<object> selectedValues, int totalOptionsCount)
{
string text;
if (EditorGUI.showMixedValue)
{
text = "\u2014"; // Em Dash
}
else
{
if (selectedValues.Count == 0)
{
text = "Nothing";
}
else if (selectedValues.Count == 1)
{
text = selectedValues.First().ToString();
}
else if (selectedValues.Count == totalOptionsCount)
{
text = "Everything";
}
else
{
text = "(Mixed)";
}
}
return new GUIContent(text);
}
#endregion
#region Layout
// Some editor GUI functions on Mac throw a NRE on Unity < 2018.2. Wrap them in safer methods.
// https://forum.unity.com/threads/mac-os-various-gui-exceptions-after-os-dialogs-have-been-shown.515421/
public static void Space(float pixels)
{
try
{
GUILayout.Space(pixels);
}
catch
{
GUIUtility.ExitGUI();
}
}
public static void FlexibleSpace()
{
try
{
GUILayout.FlexibleSpace();
}
catch
{
GUIUtility.ExitGUI();
}
}
public static void BeginHorizontal(params GUILayoutOption[] options)
{
try
{
GUILayout.BeginHorizontal(options);
}
catch
{
GUIUtility.ExitGUI();
}
}
public static void BeginHorizontal(GUIStyle style, params GUILayoutOption[] options)
{
try
{
GUILayout.BeginHorizontal(style, options);
}
catch
{
GUIUtility.ExitGUI();
}
}
public static void EndHorizontal()
{
try
{
GUILayout.EndHorizontal();
}
catch
{
GUIUtility.ExitGUI();
}
}
public static void BeginVertical(params GUILayoutOption[] options)
{
try
{
GUILayout.BeginVertical(options);
}
catch
{
GUIUtility.ExitGUI();
}
}
public static void EndVertical()
{
try
{
GUILayout.EndVertical();
}
catch
{
GUIUtility.ExitGUI();
}
}
#endregion
public static float GetTextFieldAdaptiveWidth(object content, float min = 16)
{
return Mathf.Max(min, EditorStyles.textField.CalcSize(new GUIContent(content?.ToString())).x + 1);
}
}
}