AnimancerGUI.cs 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections.Generic;
  5. using UnityEditor;
  6. using UnityEngine;
  7. using Object = UnityEngine.Object;
  8. namespace Animancer.Editor
  9. {
  10. /// <summary>[Editor-Only] Various GUI utilities used throughout Animancer.</summary>
  11. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGUI
  12. public static partial class AnimancerGUI
  13. {
  14. /************************************************************************************************************************/
  15. #region Standard Values
  16. /************************************************************************************************************************/
  17. /// <summary>The highlight color used for fields showing a warning.</summary>
  18. public static readonly Color
  19. WarningFieldColor = new(1, 0.9f, 0.6f);
  20. /// <summary>The highlight color used for fields showing an error.</summary>
  21. public static readonly Color
  22. ErrorFieldColor = new(1, 0.6f, 0.6f);
  23. /// <summary>Returns a color with uniform Red, Green, and Blue values.</summary>
  24. public static Color Grey(float rgb, float alpha = 1)
  25. => new(rgb, rgb, rgb, alpha);
  26. /************************************************************************************************************************/
  27. /// <summary><see cref="GUILayout.ExpandWidth"/> set to false.</summary>
  28. public static readonly GUILayoutOption[]
  29. DontExpandWidth = { GUILayout.ExpandWidth(false) };
  30. /************************************************************************************************************************/
  31. /// <summary>Returns <see cref="EditorGUIUtility.singleLineHeight"/>.</summary>
  32. public static float LineHeight => EditorGUIUtility.singleLineHeight;
  33. /// <summary>
  34. /// Calculates the number of vertical pixels required to draw the specified `lineCount` using the
  35. /// <see cref="LineHeight"/> and <see cref="StandardSpacing"/>.
  36. /// </summary>
  37. public static float CalculateHeight(int lineCount)
  38. => lineCount <= 0
  39. ? 0
  40. : LineHeight * lineCount + StandardSpacing * (lineCount - 1);
  41. /************************************************************************************************************************/
  42. /// <summary>Returns <see cref="EditorGUIUtility.standardVerticalSpacing"/>.</summary>
  43. public static float StandardSpacing => EditorGUIUtility.standardVerticalSpacing;
  44. /************************************************************************************************************************/
  45. private static float _IndentSize = float.NaN;
  46. /// <summary>
  47. /// The number of pixels of indentation for each <see cref="EditorGUI.indentLevel"/> increment.
  48. /// </summary>
  49. public static float IndentSize
  50. {
  51. get
  52. {
  53. if (float.IsNaN(_IndentSize))
  54. {
  55. var indentLevel = EditorGUI.indentLevel;
  56. EditorGUI.indentLevel = 1;
  57. _IndentSize = EditorGUI.IndentedRect(default).x;
  58. EditorGUI.indentLevel = indentLevel;
  59. }
  60. return _IndentSize;
  61. }
  62. }
  63. /************************************************************************************************************************/
  64. private static float _ToggleWidth = -1;
  65. /// <summary>The width of a standard <see cref="GUISkin.toggle"/> with no label.</summary>
  66. public static float ToggleWidth
  67. {
  68. get
  69. {
  70. if (_ToggleWidth == -1)
  71. _ToggleWidth = GUI.skin.toggle.CalculateWidth(GUIContent.none);
  72. return _ToggleWidth;
  73. }
  74. }
  75. /************************************************************************************************************************/
  76. /// <summary>The color of the standard label text.</summary>
  77. public static Color TextColor
  78. => GUI.skin.label.normal.textColor;
  79. /************************************************************************************************************************/
  80. private static GUIStyle _MiniButtonStyle;
  81. /// <summary>A more compact <see cref="EditorStyles.miniButton"/> with a fixed size as a tiny box.</summary>
  82. public static GUIStyle MiniButtonStyle
  83. => _MiniButtonStyle ??= new(EditorStyles.miniButton)
  84. {
  85. margin = new(0, 0, 2, 0),
  86. padding = new(2, 3, 2, 2),
  87. alignment = TextAnchor.MiddleCenter,
  88. fixedHeight = LineHeight,
  89. fixedWidth = LineHeight - 1,
  90. };
  91. private static GUIStyle _NoPaddingButtonStyle;
  92. /// <summary><see cref="MiniButtonStyle"/> with no <see cref="GUIStyle.padding"/>.</summary>
  93. public static GUIStyle NoPaddingButtonStyle
  94. => _NoPaddingButtonStyle ??= new(MiniButtonStyle)
  95. {
  96. padding = new(),
  97. fixedWidth = LineHeight,
  98. };
  99. /************************************************************************************************************************/
  100. private static GUIStyle _RightLabelStyle;
  101. /// <summary><see cref="EditorStyles.label"/> using <see cref="TextAnchor.MiddleRight"/>.</summary>
  102. public static GUIStyle RightLabelStyle
  103. => _RightLabelStyle ??= new(EditorStyles.label)
  104. {
  105. alignment = TextAnchor.MiddleRight,
  106. };
  107. /************************************************************************************************************************/
  108. private static GUIStyle _MiniButtonNoPadding;
  109. /// <summary>A more compact <see cref="EditorStyles.miniButton"/> with no padding for its content.</summary>
  110. public static GUIStyle MiniButtonNoPadding
  111. {
  112. get
  113. {
  114. _MiniButtonNoPadding ??= new(EditorStyles.miniButton)
  115. {
  116. padding = new(),
  117. overflow = new(),
  118. };
  119. return _MiniButtonNoPadding;
  120. }
  121. }
  122. /************************************************************************************************************************/
  123. /// <summary>Constants used by <see cref="Event.commandName"/>.</summary>
  124. /// <remarks>Key combinations are listed for Windows. Other platforms may differ.</remarks>
  125. public static class Commands
  126. {
  127. /************************************************************************************************************************/
  128. /// <summary><see cref="KeyCode.Delete"/></summary>
  129. public const string SoftDelete = "SoftDelete";
  130. /// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.Delete"/></summary>
  131. public const string Delete = "Delete";
  132. /// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.C"/></summary>
  133. public const string Copy = "Copy";
  134. /// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.X"/></summary>
  135. public const string Cut = "Cut";
  136. /// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.V"/></summary>
  137. public const string Paste = "Paste";
  138. /// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.D"/></summary>
  139. public const string Duplicate = "Duplicate";
  140. /// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.A"/></summary>
  141. public const string SelectAll = "SelectAll";
  142. /// <summary><see cref="KeyCode.F"/></summary>
  143. public const string FrameSelected = "FrameSelected";
  144. /// <summary><see cref="KeyCode.LeftShift"/> + <see cref="KeyCode.F"/></summary>
  145. public const string FrameSelectedWithLock = "FrameSelectedWithLock";
  146. /// <summary><see cref="KeyCode.LeftControl"/> + <see cref="KeyCode.F"/></summary>
  147. public const string Find = "Find";
  148. /************************************************************************************************************************/
  149. }
  150. /************************************************************************************************************************/
  151. #endregion
  152. /************************************************************************************************************************/
  153. #region Layout
  154. /************************************************************************************************************************/
  155. /// <summary>The offset currently applied to the GUI by <see cref="GUI.BeginGroup(Rect)"/>.</summary>
  156. public static Vector2 GuiOffset { get; set; }
  157. /************************************************************************************************************************/
  158. /// <summary>Calls <see cref="UnityEditorInternal.InternalEditorUtility.RepaintAllViews"/>.</summary>
  159. public static void RepaintEverything()
  160. => UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
  161. /************************************************************************************************************************/
  162. /// <summary><see cref="GUILayoutUtility.GetRect(float, float)"/></summary>
  163. public static Rect LayoutRect(float height)
  164. => GUILayoutUtility.GetRect(0, height);
  165. /// <summary><see cref="GUILayoutUtility.GetRect(float, float, GUIStyle)"/></summary>
  166. public static Rect LayoutRect(float height, GUIStyle style)
  167. => GUILayoutUtility.GetRect(0, height, style);
  168. /************************************************************************************************************************/
  169. /// <summary>Indicates where <see cref="LayoutSingleLineRect"/> should add the <see cref="StandardSpacing"/>.</summary>
  170. public enum SpacingMode
  171. {
  172. /// <summary>No extra space.</summary>
  173. None,
  174. /// <summary>Add extra space before the new area.</summary>
  175. Before,
  176. /// <summary>Add extra space after the new area.</summary>
  177. After,
  178. /// <summary>Add extra space before and after the new area.</summary>
  179. BeforeAndAfter
  180. }
  181. /// <summary>
  182. /// Uses <see cref="GUILayoutUtility.GetRect(float, float)"/> to get a <see cref="Rect"/> with the specified
  183. /// `height` and the <see cref="StandardSpacing"/> added according to the specified `spacing`.
  184. /// </summary>
  185. public static Rect LayoutRect(float height, SpacingMode spacing)
  186. {
  187. Rect rect;
  188. switch (spacing)
  189. {
  190. case SpacingMode.None:
  191. return LayoutRect(height);
  192. case SpacingMode.Before:
  193. rect = LayoutRect(height + StandardSpacing);
  194. rect.yMin += StandardSpacing;
  195. return rect;
  196. case SpacingMode.After:
  197. rect = LayoutRect(height + StandardSpacing);
  198. rect.height -= StandardSpacing;
  199. return rect;
  200. case SpacingMode.BeforeAndAfter:
  201. rect = LayoutRect(height + StandardSpacing * 2);
  202. rect.yMin += StandardSpacing;
  203. rect.height -= StandardSpacing;
  204. return rect;
  205. default:
  206. throw new ArgumentException($"Unsupported {nameof(StandardSpacing)}: " + spacing, nameof(spacing));
  207. }
  208. }
  209. /// <summary>
  210. /// Uses <see cref="GUILayoutUtility.GetRect(float, float)"/> to get a <see cref="Rect"/> occupying a single
  211. /// standard line with the <see cref="StandardSpacing"/> added according to the specified `spacing`.
  212. /// </summary>
  213. public static Rect LayoutSingleLineRect(SpacingMode spacing = SpacingMode.None)
  214. => LayoutRect(LineHeight, spacing);
  215. /************************************************************************************************************************/
  216. /// <summary>
  217. /// If the <see cref="Rect.height"/> is positive, this method moves the <see cref="Rect.y"/> by that amount and
  218. /// adds the <see cref="StandardSpacing"/>.
  219. /// </summary>
  220. public static void NextVerticalArea(ref Rect area)
  221. {
  222. if (area.height > 0)
  223. area.y += area.height + StandardSpacing;
  224. }
  225. /************************************************************************************************************************/
  226. /// <summary>
  227. /// Subtracts the `width` from the left side of the `area`
  228. /// and returns a new <see cref="Rect"/> occupying the removed section.
  229. /// </summary>
  230. public static Rect StealFromLeft(ref Rect area, float width, float padding = 0)
  231. {
  232. var newRect = new Rect(area.x, area.y, width, area.height);
  233. area.xMin += width + padding;
  234. return newRect;
  235. }
  236. /// <summary>
  237. /// Subtracts the `width` from the right side of the `area`
  238. /// and returns a new <see cref="Rect"/> occupying the removed section.
  239. /// </summary>
  240. public static Rect StealFromRight(ref Rect area, float width, float padding = 0)
  241. {
  242. area.width -= width + padding;
  243. return new(area.xMax + padding, area.y, width, area.height);
  244. }
  245. /// <summary>
  246. /// Subtracts the `height` from the top side of the `area`
  247. /// and returns a new <see cref="Rect"/> occupying the removed section.
  248. /// </summary>
  249. public static Rect StealFromTop(ref Rect area, float height, float padding = 0)
  250. {
  251. var newRect = new Rect(area.x, area.y, area.width, height);
  252. area.yMin += height + padding;
  253. return newRect;
  254. }
  255. /************************************************************************************************************************/
  256. /// <summary>
  257. /// Subtracts the <see cref="LineHeight"/> from the top side of the `area`
  258. /// and returns a new <see cref="Rect"/> occupying the removed section.
  259. /// </summary>
  260. public static Rect StealLineFromTop(ref Rect area)
  261. => StealFromTop(ref area, LineHeight, StandardSpacing);
  262. /************************************************************************************************************************/
  263. /// <summary>
  264. /// Returns a copy of the `rect` expanded by the specified `amount`
  265. /// (or contracted if negative).
  266. /// </summary>
  267. public static Rect Expand(this Rect rect, float amount)
  268. => new(
  269. rect.x - amount,
  270. rect.y - amount,
  271. rect.width + amount * 2,
  272. rect.height + amount * 2);
  273. /// <summary>
  274. /// Returns a copy of the `rect` expanded by the specified amounts
  275. /// on each axis (or contracted if negative).
  276. /// </summary>
  277. public static Rect Expand(this Rect rect, float x, float y)
  278. => new(
  279. rect.x - x,
  280. rect.y - y,
  281. rect.width + x * 2,
  282. rect.height + y * 2);
  283. /************************************************************************************************************************/
  284. /// <summary>Returns a copy of the `rect` expanded to include the `other`.</summary>
  285. public static Rect Encapsulate(this Rect rect, Rect other)
  286. => Rect.MinMaxRect(
  287. Math.Min(rect.xMin, other.xMin),
  288. Math.Min(rect.yMin, other.yMin),
  289. Math.Max(rect.xMax, other.xMax),
  290. Math.Max(rect.yMax, other.yMax));
  291. /************************************************************************************************************************/
  292. /// <summary>
  293. /// Divides the given `area` such that the fields associated with both labels will have equal space
  294. /// remaining after the labels themselves.
  295. /// </summary>
  296. public static void SplitHorizontally(
  297. Rect area,
  298. string label0,
  299. string label1,
  300. out float width0,
  301. out float width1,
  302. out Rect rect0,
  303. out Rect rect1)
  304. {
  305. width0 = CalculateLabelWidth(label0);
  306. width1 = CalculateLabelWidth(label1);
  307. const float Padding = 1;
  308. rect0 = rect1 = area;
  309. var remainingWidth = area.width - width0 - width1 - Padding;
  310. rect0.width = width0 + remainingWidth * 0.5f;
  311. rect1.xMin = rect0.xMax + Padding;
  312. }
  313. /************************************************************************************************************************/
  314. /// <summary>[Animancer Extension] Calls <see cref="GUIStyle.CalcMinMaxWidth"/> and returns the max width.</summary>
  315. public static float CalculateWidth(this GUIStyle style, GUIContent content)
  316. {
  317. style.CalcMinMaxWidth(content, out _, out var width);
  318. return Mathf.Ceil(width);
  319. }
  320. /// <summary>[Animancer Extension] Calls <see cref="GUIStyle.CalcMinMaxWidth"/> and returns the max width.</summary>
  321. public static float CalculateWidth(this GUIStyle style, string text)
  322. {
  323. using (var content = PooledGUIContent.Acquire(text))
  324. return style.CalculateWidth(content);
  325. }
  326. /************************************************************************************************************************/
  327. private static ConversionCache<string, float> _LabelWidthCache;
  328. /// <summary>
  329. /// Calls <see cref="GUIStyle.CalcMinMaxWidth"/> using <see cref="GUISkin.label"/> and returns the max
  330. /// width. The result is cached for efficient reuse.
  331. /// </summary>
  332. public static float CalculateLabelWidth(string text)
  333. {
  334. _LabelWidthCache ??= ConversionCache.CreateWidthCache(GUI.skin.label);
  335. return _LabelWidthCache.Convert(text);
  336. }
  337. /************************************************************************************************************************/
  338. private static string[] _IntToStringCache;
  339. /// <summary>Caches and returns <see cref="int.ToString()"/> if <c>0 &lt;= value &lt; 100</c>.</summary>
  340. public static string ToStringCached(this int value)
  341. {
  342. const int CacheSize = 100;
  343. if (value < 0 || value >= CacheSize)
  344. return value.ToString();
  345. if (_IntToStringCache == null)
  346. {
  347. _IntToStringCache = new string[CacheSize];
  348. for (int i = 0; i < _IntToStringCache.Length; i++)
  349. _IntToStringCache[i] = i.ToString();
  350. }
  351. return _IntToStringCache[value];
  352. }
  353. /************************************************************************************************************************/
  354. /// <summary>
  355. /// Begins a vertical layout group using the given style and decreases the
  356. /// <see cref="EditorGUIUtility.labelWidth"/> to compensate for the indentation.
  357. /// </summary>
  358. public static void BeginVerticalBox(GUIStyle style)
  359. {
  360. if (style == null)
  361. {
  362. GUILayout.BeginVertical();
  363. return;
  364. }
  365. GUILayout.BeginVertical(style);
  366. EditorGUIUtility.labelWidth -= style.padding.left;
  367. }
  368. /// <summary>
  369. /// Ends a layout group started by <see cref="BeginVerticalBox"/> and restores the
  370. /// <see cref="EditorGUIUtility.labelWidth"/>.
  371. /// </summary>
  372. public static void EndVerticalBox(GUIStyle style)
  373. {
  374. if (style != null)
  375. EditorGUIUtility.labelWidth += style.padding.left;
  376. GUILayout.EndVertical();
  377. }
  378. /************************************************************************************************************************/
  379. private static Func<Rect> _GetGUIClipRect;
  380. /// <summary>Returns the <see cref="Rect"/> of the current <see cref="GUI.BeginClip(Rect)"/>.</summary>
  381. public static Rect GetGUIClipRect()
  382. {
  383. if (_GetGUIClipRect != null)
  384. return _GetGUIClipRect();
  385. var type = typeof(GUI).Assembly.GetType("UnityEngine.GUIClip");
  386. var method = type?.GetMethod("GetTopRect", AnimancerReflection.AnyAccessBindings);
  387. if (method != null &&
  388. method.ReturnType != null &&
  389. method.GetParameters().Length == 0)
  390. {
  391. _GetGUIClipRect = (Func<Rect>)Delegate.CreateDelegate(typeof(Func<Rect>), method);
  392. }
  393. else
  394. {
  395. _GetGUIClipRect = () => new(0, 0, Screen.width, Screen.height);
  396. }
  397. return _GetGUIClipRect();
  398. }
  399. /************************************************************************************************************************/
  400. #endregion
  401. /************************************************************************************************************************/
  402. #region Labels
  403. /************************************************************************************************************************/
  404. private static GUIStyle _WeightLabelStyle;
  405. private static float _WeightLabelWidth = -1;
  406. /// <summary>
  407. /// Draws a label showing the `weight` aligned to the right side of the `area` and reduces its
  408. /// <see cref="Rect.width"/> to remove that label from its area.
  409. /// </summary>
  410. public static void DoWeightLabel(ref Rect area, float weight, float effectiveWeight)
  411. {
  412. var label = WeightToShortString(weight, out var isExact);
  413. _WeightLabelStyle ??= new(GUI.skin.label)
  414. {
  415. alignment = TextAnchor.MiddleRight,
  416. };
  417. if (_WeightLabelWidth < 0)
  418. {
  419. _WeightLabelStyle.fontStyle = FontStyle.Italic;
  420. _WeightLabelWidth = _WeightLabelStyle.CalculateWidth("0.0");
  421. }
  422. _WeightLabelStyle.normal.textColor = Color.Lerp(Color.grey, TextColor, 0.2f + effectiveWeight * 0.8f);
  423. _WeightLabelStyle.fontStyle = isExact ? FontStyle.Normal : FontStyle.Italic;
  424. var weightArea = StealFromRight(ref area, _WeightLabelWidth);
  425. GUI.Label(weightArea, label, _WeightLabelStyle);
  426. }
  427. /************************************************************************************************************************/
  428. private static ConversionCache<float, string> _ShortWeightCache;
  429. /// <summary>Returns a string which approximates the `weight` into no more than 3 digits.</summary>
  430. private static string WeightToShortString(float weight, out bool isExact)
  431. {
  432. isExact = true;
  433. if (weight == 0)
  434. return "0.0";
  435. if (weight == 1)
  436. return "1.0";
  437. isExact = false;
  438. if (weight >= -0.5f && weight < 0.05f)
  439. return "~0.";
  440. if (weight >= 0.95f && weight < 1.05f)
  441. return "~1.";
  442. if (weight <= -99.5f)
  443. return "-??";
  444. if (weight >= 999.5f)
  445. return "???";
  446. _ShortWeightCache ??= new(value =>
  447. {
  448. if (value < -9.5f) return $"{value:F0}";
  449. if (value < -0.5f) return $"{value:F0}.";
  450. if (value < 9.5f) return $"{value:F1}";
  451. if (value < 99.5f) return $"{value:F0}.";
  452. return $"{value:F0}";
  453. });
  454. var rounded = weight > 0 ? Mathf.Floor(weight * 10) : Mathf.Ceil(weight * 10);
  455. isExact = Mathf.Approximately(weight * 10, rounded);
  456. return _ShortWeightCache.Convert(weight);
  457. }
  458. /************************************************************************************************************************/
  459. /// <summary>The <see cref="EditorGUIUtility.labelWidth"/> from before <see cref="BeginTightLabel"/>.</summary>
  460. private static float _TightLabelWidth;
  461. /// <summary>
  462. /// Stores the <see cref="EditorGUIUtility.labelWidth"/> and changes it to the exact width of the `label`.
  463. /// </summary>
  464. public static string BeginTightLabel(string label)
  465. {
  466. _TightLabelWidth = EditorGUIUtility.labelWidth;
  467. EditorGUIUtility.labelWidth = CalculateLabelWidth(label) + EditorGUI.indentLevel * IndentSize;
  468. return label;
  469. }
  470. /// <summary>Reverts <see cref="EditorGUIUtility.labelWidth"/> to its previous value.</summary>
  471. public static void EndTightLabel()
  472. {
  473. EditorGUIUtility.labelWidth = _TightLabelWidth;
  474. }
  475. /************************************************************************************************************************/
  476. /// <summary>Draws a button using <see cref="EditorStyles.miniButton"/> and <see cref="DontExpandWidth"/>.</summary>
  477. public static bool CompactMiniButton(GUIContent content)
  478. => GUILayout.Button(content, EditorStyles.miniButton, DontExpandWidth);
  479. /// <summary>Draws a button using <see cref="EditorStyles.miniButton"/>.</summary>
  480. public static bool CompactMiniButton(Rect area, GUIContent content)
  481. => GUI.Button(area, content, EditorStyles.miniButton);
  482. /************************************************************************************************************************/
  483. #endregion
  484. /************************************************************************************************************************/
  485. #region Fields
  486. /************************************************************************************************************************/
  487. /// <summary>Draws a label field with a foldout.</summary>
  488. public static bool DoLabelFoldoutFieldGUI(string label, string value, bool isExpanded)
  489. {
  490. using (var labelContent = PooledGUIContent.Acquire(label))
  491. using (var valueContent = PooledGUIContent.Acquire(value))
  492. return DoLabelFoldoutFieldGUI(labelContent, valueContent, isExpanded);
  493. }
  494. /// <summary>Draws a label field with a foldout.</summary>
  495. public static bool DoLabelFoldoutFieldGUI(GUIContent label, GUIContent value, bool isExpanded)
  496. {
  497. var area = LayoutSingleLineRect();
  498. EditorGUI.LabelField(area, label, value);
  499. return EditorGUI.Foldout(area, isExpanded, "", true);
  500. }
  501. /// <summary>Draws a foldout which stores its state in a hash set.</summary>
  502. public static bool DoHashedFoldoutGUI<T>(Rect area, HashSet<T> expandedItems, T item)
  503. {
  504. var wasExpanded = expandedItems.Contains(item);
  505. var isExpanded = EditorGUI.Foldout(area, wasExpanded, "", true);
  506. if (isExpanded != wasExpanded)
  507. if (isExpanded)
  508. expandedItems.Add(item);
  509. else
  510. expandedItems.Remove(item);
  511. return isExpanded;
  512. }
  513. /************************************************************************************************************************/
  514. /// <summary>Draws an object reference field.</summary>
  515. public static T DoObjectFieldGUI<T>(
  516. Rect area,
  517. GUIContent label,
  518. T value,
  519. bool allowSceneObjects)
  520. where T : Object
  521. => EditorGUI.ObjectField(area, label, value, typeof(T), allowSceneObjects) as T;
  522. /// <summary>Draws an object reference field.</summary>
  523. public static T DoObjectFieldGUI<T>(
  524. Rect area,
  525. string label,
  526. T value,
  527. bool allowSceneObjects)
  528. where T : Object
  529. {
  530. using var content = PooledGUIContent.Acquire(label);
  531. return DoObjectFieldGUI(area, content, value, allowSceneObjects);
  532. }
  533. /************************************************************************************************************************/
  534. /// <summary>Draws an object reference field.</summary>
  535. public static T DoObjectFieldGUI<T>(
  536. GUIContent label,
  537. T value,
  538. bool allowSceneObjects)
  539. where T : Object
  540. {
  541. var height = EditorGUIUtility.HasObjectThumbnail(typeof(T)) ? 64f : LineHeight;
  542. var area = EditorGUILayout.GetControlRect(label != null, height);
  543. return DoObjectFieldGUI(area, label, value, allowSceneObjects);
  544. }
  545. /// <summary>Draws an object reference field.</summary>
  546. public static T DoObjectFieldGUI<T>(
  547. string label,
  548. T value,
  549. bool allowSceneObjects)
  550. where T : Object
  551. {
  552. using var content = PooledGUIContent.Acquire(label);
  553. return DoObjectFieldGUI(content, value, allowSceneObjects);
  554. }
  555. /************************************************************************************************************************/
  556. /// <summary>
  557. /// Draws an object reference field with a dropdown button as its label
  558. /// and returns true if clicked.
  559. /// </summary>
  560. public static bool DoDropdownObjectFieldGUI<T>(
  561. Rect area,
  562. GUIContent label,
  563. bool showDropdown,
  564. ref T value)
  565. where T : Object
  566. {
  567. var labelWidth = EditorGUIUtility.labelWidth;
  568. labelWidth += 2;
  569. area.xMin -= 1;
  570. var spacing = StandardSpacing;
  571. var labelArea = StealFromLeft(ref area, labelWidth - spacing, spacing);
  572. value = DoObjectFieldGUI(area, "", value, true);
  573. if (showDropdown)
  574. {
  575. return EditorGUI.DropdownButton(labelArea, label, FocusType.Passive);
  576. }
  577. else
  578. {
  579. GUI.Label(labelArea, label);
  580. return false;
  581. }
  582. }
  583. /************************************************************************************************************************/
  584. #endregion
  585. /************************************************************************************************************************/
  586. #region Events
  587. /************************************************************************************************************************/
  588. /// <summary>Sets <see cref="GUI.changed"/> if `guiChanged` is <c>true</c>.</summary>
  589. public static void SetGuiChanged(bool guiChanged)
  590. {
  591. if (guiChanged)
  592. GUI.changed = true;
  593. }
  594. /************************************************************************************************************************/
  595. /// <summary>
  596. /// Calls <see cref="Event.Use"/> and sets the
  597. /// <see cref="GUI.changed"/> and <see cref="GUIUtility.hotControl"/>.
  598. /// </summary>
  599. public static void Use(this Event guiEvent, int controlId, bool guiChanged = false)
  600. {
  601. SetGuiChanged(guiChanged);
  602. GUIUtility.hotControl = controlId;
  603. guiEvent.Use();
  604. }
  605. /************************************************************************************************************************/
  606. /// <summary>
  607. /// Sets the <see cref="GUIUtility.hotControl"/> and uses the `currentEvent`
  608. /// if the mouse position is inside the `area`.
  609. /// </summary>
  610. /// <remarks>This method is useful for handling <see cref="EventType.MouseDown"/>.</remarks>
  611. public static bool TryUseMouseDown(Rect area, Event currentEvent, int controlID)
  612. {
  613. if (!area.Contains(currentEvent.mousePosition))
  614. return false;
  615. GUIUtility.keyboardControl = 0;
  616. GUIUtility.hotControl = controlID;
  617. currentEvent.Use();
  618. return true;
  619. }
  620. /************************************************************************************************************************/
  621. /// <summary>
  622. /// Releases the <see cref="GUIUtility.hotControl"/> and uses the `currentEvent` if it was the active control.
  623. /// </summary>
  624. /// <remarks>This method is useful for handling <see cref="EventType.MouseUp"/>.</remarks>
  625. public static bool TryUseMouseUp(Event currentEvent, int controlID, bool guiChanged = false)
  626. {
  627. if (GUIUtility.hotControl != controlID)
  628. return false;
  629. GUIUtility.hotControl = 0;
  630. currentEvent.Use();
  631. SetGuiChanged(guiChanged);
  632. return true;
  633. }
  634. /************************************************************************************************************************/
  635. /// <summary>
  636. /// Uses the `currentEvent` and sets <see cref="GUI.changed"/>
  637. /// if the `controlID` matches the <see cref="GUIUtility.hotControl"/>.
  638. /// </summary>
  639. /// <remarks>This method is useful for handling <see cref="EventType.MouseDrag"/>.</remarks>
  640. public static bool TryUseHotControl(Event currentEvent, int controlID, bool guiChanged = true)
  641. {
  642. if (GUIUtility.hotControl != controlID)
  643. return false;
  644. SetGuiChanged(guiChanged);
  645. currentEvent.Use();
  646. return true;
  647. }
  648. /************************************************************************************************************************/
  649. /// <summary>
  650. /// Uses the `currentEvent` if the `controlID` has <see cref="GUIUtility.keyboardControl"/>.
  651. /// If a `key` is specified, other keys will be ignored.
  652. /// </summary>
  653. /// <remarks>
  654. /// This method is useful for handling
  655. /// <see cref="EventType.KeyDown"/> and <see cref="EventType.KeyUp"/>.
  656. /// </remarks>
  657. public static bool TryUseKey(Event currentEvent, int controlID, KeyCode key = KeyCode.None)
  658. {
  659. if (GUIUtility.keyboardControl != controlID)
  660. return false;
  661. if (key != KeyCode.None && currentEvent.keyCode != key)
  662. return false;
  663. currentEvent.Use();
  664. GUI.changed = true;
  665. return true;
  666. }
  667. /************************************************************************************************************************/
  668. /// <summary>
  669. /// Returns true and uses the current event if it is
  670. /// <see cref="EventType.MouseUp"/> inside the specified `area`.
  671. /// </summary>
  672. /// <remarks>Uses <see cref="EventType.MouseDown"/> and <see cref="EventType.MouseUp"/> events.</remarks>
  673. public static bool TryUseClickEvent(Rect area, int button = -1, int controlID = 0)
  674. {
  675. if (controlID == 0)
  676. controlID = GUIUtility.GetControlID(FocusType.Passive);
  677. var currentEvent = Event.current;
  678. if (button >= 0 && currentEvent.button != button)
  679. return false;
  680. switch (currentEvent.type)
  681. {
  682. case EventType.MouseDown:
  683. TryUseMouseDown(area, currentEvent, controlID);
  684. break;
  685. case EventType.MouseUp:
  686. return TryUseMouseUp(currentEvent, controlID, true) && area.Contains(currentEvent.mousePosition);
  687. }
  688. return false;
  689. }
  690. /// <summary>
  691. /// Returns true and uses the current event if it is <see cref="EventType.MouseUp"/> inside the last GUI Layout
  692. /// <see cref="Rect"/> that was drawn.
  693. /// </summary>
  694. public static bool TryUseClickEventInLastRect(int button = -1)
  695. => TryUseClickEvent(GUILayoutUtility.GetLastRect(), button);
  696. /************************************************************************************************************************/
  697. /// <summary>Is the `currentEvent` a Middle Click or Alt + Left Click? </summary>
  698. public static bool IsMiddleClick(this Event currentEvent)
  699. => currentEvent.button == 2
  700. || (currentEvent.button == 0 && currentEvent.modifiers == EventModifiers.Alt);
  701. /************************************************************************************************************************/
  702. /// <summary>Deselects any selected IMGUI control.</summary>
  703. public static void Deselect()
  704. {
  705. GUIUtility.hotControl = 0;
  706. GUIUtility.keyboardControl = 0;
  707. }
  708. /************************************************************************************************************************/
  709. #endregion
  710. /************************************************************************************************************************/
  711. #region Other
  712. /************************************************************************************************************************/
  713. /// <summary>Draws a line.</summary>
  714. /// <remarks>
  715. /// Use <see cref="BeginTriangles"/>, <see cref="DrawLineBatched"/>, and <see cref="EndTriangles"/>
  716. /// if you want to draw multiple lines more efficiently.
  717. /// </remarks>
  718. public static void DrawLine(
  719. Vector2 a,
  720. Vector2 b,
  721. float width,
  722. Color color)
  723. {
  724. BeginTriangles(color);
  725. DrawLineBatched(a, b, width);
  726. EndTriangles();
  727. }
  728. /************************************************************************************************************************/
  729. /// <summary>Sets up the rendering details for <see cref="DrawLineBatched"/>.</summary>
  730. /// <remarks>
  731. /// If the color doesn't work correctly, you may need to call
  732. /// <see cref="Handles.DrawLine(Vector3, Vector3)"/> before this.
  733. /// </remarks>
  734. public static void BeginTriangles(Color color)
  735. {
  736. GL.Begin(GL.TRIANGLES);
  737. GL.Color(color);
  738. }
  739. /// <summary>Cleans up the rendering details for <see cref="DrawLineBatched"/>.</summary>
  740. public static void EndTriangles()
  741. {
  742. GL.End();
  743. }
  744. /************************************************************************************************************************/
  745. /// <summary>Draws a line.</summary>
  746. /// <remarks>Must be called after <see cref="BeginTriangles"/> and before <see cref="EndTriangles"/>.</remarks>
  747. public static void DrawLineBatched(
  748. Vector2 a,
  749. Vector2 b,
  750. float width)
  751. {
  752. var perpendicular = 0.5f * width * (a - b).GetPerpendicular().normalized;
  753. var a0 = a - perpendicular;
  754. var a1 = a + perpendicular;
  755. var b0 = b - perpendicular;
  756. var b1 = b + perpendicular;
  757. GL.Vertex(a0);
  758. GL.Vertex(a1);
  759. GL.Vertex(b0);
  760. GL.Vertex(a1);
  761. GL.Vertex(b0);
  762. GL.Vertex(b1);
  763. }
  764. /************************************************************************************************************************/
  765. /// <summary>Draws triangular arrow.</summary>
  766. /// <remarks>Must be called after <see cref="BeginTriangles"/> and before <see cref="EndTriangles"/>.</remarks>
  767. public static void DrawArrowTriangleBatched(
  768. Vector2 point,
  769. Vector2 direction,
  770. float width,
  771. float length)
  772. {
  773. direction.Normalize();
  774. var perpendicular = 0.5f * width * direction.GetPerpendicular();
  775. // These commented out bits would use the point as the center of the triangle instead.
  776. direction *= length;// * 0.5f;
  777. var back = point - direction;
  778. GL.Vertex(point);// + direction);
  779. GL.Vertex(back + perpendicular);
  780. GL.Vertex(back - perpendicular);
  781. }
  782. /************************************************************************************************************************/
  783. /// <summary>Returns a vector perpendicular to the given value with the same magnitude.</summary>
  784. public static Vector2 GetPerpendicular(this Vector2 vector)
  785. => new(vector.y, -vector.x);
  786. /************************************************************************************************************************/
  787. /// <summary>Draws a `sprite` in the given `area`.</summary>
  788. public static void DrawSprite(Rect area, Sprite sprite)
  789. {
  790. var texture = sprite.texture;
  791. var textureWidth = texture.width;
  792. var textureHeight = texture.height;
  793. var spriteRect = sprite.rect;
  794. spriteRect.x /= textureWidth;
  795. spriteRect.y /= textureHeight;
  796. spriteRect.width /= textureWidth;
  797. spriteRect.height /= textureHeight;
  798. GUI.DrawTextureWithTexCoords(
  799. area,
  800. texture,
  801. spriteRect);
  802. }
  803. /************************************************************************************************************************/
  804. /// <summary>Returns a colour with its hue based on the `hash`.</summary>
  805. public static Color GetHashColor(int hash, float s = 1, float v = 1, float a = 1)
  806. {
  807. uint uHash = (uint)hash;
  808. double dHash = (double)uHash / uint.MaxValue;
  809. float h = (float)dHash;
  810. var color = Color.HSVToRGB(h, s, v);
  811. color.a = a;
  812. return color;
  813. }
  814. /************************************************************************************************************************/
  815. /// <summary>Clears the <see cref="Selection.objects"/> then returns it to its current state.</summary>
  816. /// <remarks>
  817. /// This forces the <see cref="UnityEditorInternal.ReorderableList"/> drawer to adjust to height changes which
  818. /// it unfortunately doesn't do on its own..
  819. /// </remarks>
  820. public static void ReSelectCurrentObjects()
  821. {
  822. var selection = Selection.objects;
  823. Selection.objects = Array.Empty<Object>();
  824. EditorApplication.delayCall += () =>
  825. EditorApplication.delayCall += () =>
  826. Selection.objects = selection;
  827. }
  828. /************************************************************************************************************************/
  829. /// <summary>Draws a button which toggles between play and pause icons.</summary>
  830. public static bool DoPlayPauseToggle(
  831. Rect area,
  832. bool isPlaying,
  833. GUIStyle style = null,
  834. string tooltip = null)
  835. {
  836. var content = isPlaying
  837. ? AnimancerIcons.PauseIcon
  838. : AnimancerIcons.PlayIcon;
  839. var oldTooltip = content.tooltip;
  840. content.tooltip = tooltip;
  841. style ??= MiniButtonNoPadding;
  842. if (GUI.Button(area, content, style))
  843. isPlaying = !isPlaying;
  844. content.tooltip = oldTooltip;
  845. return isPlaying;
  846. }
  847. /************************************************************************************************************************/
  848. #endregion
  849. /************************************************************************************************************************/
  850. }
  851. }
  852. #endif