Skip to content

Instantly share code, notes, and snippets.

@SolarianZ
Created May 1, 2025 07:54
Show Gist options
  • Select an option

  • Save SolarianZ/b305b160e46a7e958502989da46e9139 to your computer and use it in GitHub Desktop.

Select an option

Save SolarianZ/b305b160e46a7e958502989da46e9139 to your computer and use it in GitHub Desktop.
{"category": "Unity Engine/Editor/Extensions", "keywords": "Unity, Editor, Searchable, PopupField"} Display a searchable PopupField in the Unity Editor.
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using PopupWindow = UnityEditor.PopupWindow;
public class SearchablePopupField<T> : PopupField<T>
{
#region ctor
public SearchablePopupField(string label = null) : base(label)
{
}
public SearchablePopupField(string label, List<T> choices, T defaultValue,
Func<T, string> formatSelectedValueCallback = null,
Func<T, string> formatListItemCallback = null)
: base(label, choices, defaultValue, formatSelectedValueCallback, formatListItemCallback)
{
}
public SearchablePopupField(List<T> choices, T defaultValue,
Func<T, string> formatSelectedValueCallback = null,
Func<T, string> formatListItemCallback = null)
: base(choices, defaultValue, formatSelectedValueCallback, formatListItemCallback)
{
}
public SearchablePopupField(List<T> choices, int defaultIndex,
Func<T, string> formatSelectedValueCallback = null,
Func<T, string> formatListItemCallback = null)
: base(choices, defaultIndex, formatSelectedValueCallback, formatListItemCallback)
{
}
#endregion
#region Reflection
private List<T> _choicesCache;
private VisualElement _visualInputCache;
private List<T> GetChoices()
{
#if UNITY_2020_3_OR_NEWER
return choices;
#else
if (_choicesCache == null)
{
PropertyInfo choicesProp = typeof(BasePopupField<T, T>).GetProperty("choices", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(choicesProp);
_choicesCache = (List<T>)choicesProp.GetValue(this);
}
return _choicesCache;
#endif
}
private VisualElement GetVisualInput()
{
if (_visualInputCache == null)
{
PropertyInfo visualInputProp = typeof(BaseField<T>).GetProperty("visualInput", BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(visualInputProp);
_visualInputCache = (VisualElement)visualInputProp.GetValue(this);
}
return _visualInputCache;
}
#endregion
#region Block the default GenericMenu
#if UNITY_2023_1_OR_NEWER
[Obsolete]
protected override void ExecuteDefaultAction(EventBase evt)
{
if (TryHandleEventInternal(evt))
return;
base.ExecuteDefaultAction(evt);
}
#endif
#if !UNITY_2023_1_OR_NEWER
#if UNITY_2022_1_OR_NEWER
[Obsolete]
#endif
public override void HandleEvent(EventBase evt)
{
if (TryHandleEventInternal(evt))
return;
base.HandleEvent(evt);
}
#endif
private bool TryHandleEventInternal(EventBase evt)
{
if (evt is PointerDownEvent pointerDownEvent)
{
if (pointerDownEvent.button == 0 && GetVisualInput().ContainsPoint(GetVisualInput().WorldToLocal(pointerDownEvent.originalMousePosition)))
{
pointerDownEvent.StopImmediatePropagation();
Rect popupRect = GetVisualInput().worldBound;
PopupWindow.Show(popupRect, new Content(this));
return true;
}
}
return false;
}
#endregion
class Content : PopupWindowContent
{
private readonly SearchablePopupField<T> _popup;
private string _searchContent;
private Vector2 _scrollPosition;
private List<T> _filteredChoices;
private ReorderableList _list;
public Content(SearchablePopupField<T> popup)
{
_popup = popup;
}
public override void OnOpen()
{
base.OnOpen();
_filteredChoices = new List<T>(_popup.GetChoices());
_list = new ReorderableList(_filteredChoices, typeof(T))
{
index = _popup.index,
displayAdd = false,
displayRemove = false,
headerHeight = 0,
footerHeight = 0,
draggable = false,
onMouseUpCallback = OnMouseUp,
drawElementCallback = DrawElement,
drawElementBackgroundCallback = DrawElementBackground,
};
}
public override void OnGUI(Rect rect)
{
const string SEARCH_CONTROL = "ToolbarSearchField";
EditorGUI.BeginChangeCheck();
{
GUI.SetNextControlName(SEARCH_CONTROL);
_searchContent = EditorGUILayoutHelper.ToolbarSearchField(_searchContent);
EditorGUI.FocusTextInControl(SEARCH_CONTROL);
}
if (EditorGUI.EndChangeCheck())
{
_filteredChoices.Clear();
for (int i = 0; i < _popup.GetChoices().Count; i++)
{
string elemDisplayName = GetElementDisplayName(_popup.GetChoices(), i, _popup.index == i);
if (elemDisplayName.IndexOf(_searchContent, StringComparison.OrdinalIgnoreCase) >= 0)
_filteredChoices.Add(_popup.GetChoices()[i]);
}
_list.list = _filteredChoices;
}
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
_list.DoLayoutList();
EditorGUILayout.EndScrollView();
editorWindow.Repaint();
}
public override Vector2 GetWindowSize()
{
GUIContent tempLabelContent = new GUIContent();
float maxWidth = 0;
foreach (T item in _popup.GetChoices())
{
string label = item?.GetHashCode() == _popup.value?.GetHashCode()
? _popup.formatSelectedValueCallback?.Invoke(item) ?? item?.ToString() ?? string.Empty
: _popup.formatListItemCallback?.Invoke(item) ?? item?.ToString() ?? string.Empty;
tempLabelContent.text = label;
float width = EditorStyles.toolbarPopup.CalcSize(tempLabelContent).x;
if (width > maxWidth)
maxWidth = width;
}
Vector2 size = new Vector2
{
x = Mathf.Max(300, _popup.GetVisualInput().resolvedStyle.width, maxWidth),
y = Mathf.Min(400, Mathf.Max(_list.elementHeight * _list.count + 36, 48)),
};
return size;
}
private string GetElementDisplayName(IList<T> elements, int index, bool isActive)
{
T item = elements[index];
string text;
if (isActive)
text = _popup.formatSelectedValueCallback?.Invoke(item) ?? item.ToString();
else
text = _popup.formatListItemCallback?.Invoke(item) ?? item.ToString();
return text;
}
private void DrawElementBackground(Rect rect, int index, bool isActive, bool isFocused)
{
if (isActive)
{
EditorGUI.DrawRect(rect, new Color(0.24f, 0.48f, 0.90f, 0.5f));
}
else if (isFocused)
{
EditorGUI.DrawRect(rect, new Color(0.24f, 0.48f, 0.90f, 0.2f));
}
else if (rect.Contains(Event.current.mousePosition))
{
EditorGUI.DrawRect(rect, new Color(0.24f, 0.48f, 0.90f, 0.1f));
}
}
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
string text = GetElementDisplayName(_filteredChoices, index, isActive);
GUI.Label(rect, text);
}
private void OnMouseUp(ReorderableList list)
{
_popup.value = _filteredChoices[list.index];
editorWindow.Close();
}
}
}
public static class EditorGUILayoutHelper
{
#region Reflection
private static Func<string, GUILayoutOption[], string> _toolbarSearchFieldCache;
public static string ToolbarSearchField(string searchText)
{
// string EditorGUILayout.ToolbarSearchField(string);
if (_toolbarSearchFieldCache == null)
{
MethodInfo toolbarSearchFieldMethod = typeof(EditorGUILayout).GetMethod("ToolbarSearchField", BindingFlags.Static | BindingFlags.NonPublic,
null, new Type[] { typeof(string), typeof(GUILayoutOption[]) }, null);
Assert.IsNotNull(toolbarSearchFieldMethod);
_toolbarSearchFieldCache = (Func<string, GUILayoutOption[], string>)Delegate.CreateDelegate(typeof(Func<string, GUILayoutOption[], string>), toolbarSearchFieldMethod);
}
searchText = _toolbarSearchFieldCache(searchText, null);
return searchText;
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment