Skip to content

Instantly share code, notes, and snippets.

@markeahogan
Last active June 16, 2024 18:09
Show Gist options
  • Save markeahogan/adf37c77f441d4b5666299727fd547d3 to your computer and use it in GitHub Desktop.
Save markeahogan/adf37c77f441d4b5666299727fd547d3 to your computer and use it in GitHub Desktop.
Utility functions for IMGUI Rects, useful for drawing PropertyFields in a single line without lots of Rect boilerplate
using UnityEngine;
namespace PopupAsylum
{
/// <summary>
/// Utility functions for IMGUI Rects, useful for drawing PropertyFields in a single line
/// Works by returning the Rect to draw the contol and filling the out argument with a Rect for the remaining space
/// By passing the same Rect to the out member it can keep eating chunks of the property's Rect
///
/// void ExampleDrawer(SerializedProperty property, Rect rect)
/// {
/// rect = rect.Margin(5);
/// EditorGUI.PropertyField(rect.TakeSingleLine(), property.FindRelative("a"));
/// EditorGUI.PropertyField(rect.TakeSingleLine(), property.FindRelative("b"));
/// EditorGUI.PropertyField(rect.TakeSingleLine(), property.FindRelative("c"));
/// }
/// </summary>
public static class IMGUIRectExtensions
{
private static float SingleLine =>
#if UNITY_EDITOR
UnityEditor.EditorGUIUtility.singleLineHeight;
#else
13;
#endif
private static float Spacing =>
#if UNITY_EDITOR
UnityEditor.EditorGUIUtility.standardVerticalSpacing;
#else
2;
#endif
private static float LabelWidth =>
#if UNITY_EDITOR
UnityEditor.EditorGUIUtility.labelWidth;
#else
80;
#endif
/// <summary>
/// Returns a Rect inset from the paramater equally on all sides
/// </summary>
public static Rect Margin(this Rect rect, float all) => Margin(rect, all, all, all, all);
/// <summary>
/// Returns a Rect inset from the paramater equally on top/bottom and equally on left/right
/// </summary>
public static Rect Margin(this Rect rect, float leftRight, float topBottom) => Margin(rect, leftRight, leftRight, topBottom, topBottom);
/// <summary>
/// Returns a Rect inset from the paramater by the specified values
/// </summary>
public static Rect Margin(this Rect rect, float left, float right, float top, float bottom)
{
rect.height -= (top + bottom);
rect.width -= (left + right);
rect.x += left;
rect.y += top;
return rect;
}
/// <summary>
/// Returns an array of rects representing the parameter divided vertially, with optional spacing between rects
/// </summary>
public static Rect[] DivideY(this Rect rect, int count, float spacing = -1)
{
DefaultSpacing(ref spacing);
var result = new Rect[count];
result[0] = rect;
float availableSpace = rect.height - spacing * (count - 1);
result[0].height = availableSpace / count;
for (int i = 1; i < count; i++)
{
result[i] = result[0];
result[i].y += (result[0].height + spacing) * i;
}
return result;
}
/// <summary>
/// Returns an array of rects representing the parameter divided horizontally, with optional spacing between rects
/// </summary>
public static Rect[] DivideX(this Rect rect, int count, float spacing = 0)
{
DefaultSpacing(ref spacing);
var result = new Rect[count];
result[0] = rect;
float availableSpace = rect.width - spacing * (count - 1);
result[0].width = availableSpace / count;
for (int i = 1; i < count; i++)
{
result[i] = result[0];
result[i].x += (result[0].width + spacing) * i;
}
return result;
}
/// <summary>
/// Splits the rect into two at the 'height' and returns the top half, out Rect remaining will contain the bottom half
/// If height is negative it will be split relative to the bottom rather than the top
/// </summary>
public static Rect TakeTop(this ref Rect rect, float height, float spacing = -1)
{
DefaultSpacing(ref spacing);
var topHalf = rect;
var bottomHalf = rect;
topHalf.height = Mathf.Min(height < 0 ? rect.height + height : height, rect.height); // using + subtract because height is negative
bottomHalf.height = Mathf.Max(rect.height - topHalf.height - spacing, 0);
bottomHalf.y += topHalf.height + spacing;
rect = bottomHalf;
return topHalf;
}
/// <summary>
/// Splits the rect into two at the 'height' and returns the bottom half, out Rect remaining will contain the top half
/// If height is negative it will be split relative to the top rather than the bottom
/// </summary>
public static Rect TakeBottom(this ref Rect rect, float height, float spacing = 0)
{
var topHalf = TakeTop(ref rect, -height, 0);
topHalf.height -= DefaultSpacing(ref spacing);
var bottomHalf = rect;
rect = topHalf;
return bottomHalf;
}
/// <summary>
/// Splits the rect into two at the 'width' and returns the left half, out Rect remaining will contain the right half
/// If width is negative it will be split relative to the right edge rather than the left edge
/// </summary>
public static Rect TakeLeft(this ref Rect rect, float width, float spacing = 0)
{
DefaultSpacing(ref spacing);
var leftHalf = rect;
var rightHalf = rect;
leftHalf.width = Mathf.Min(width < 0 ? rect.width + width : width, rect.width); // using + to subtract because width is negative
rightHalf.width = Mathf.Max(rect.width - leftHalf.width - spacing, 0);
rightHalf.x += leftHalf.width + spacing;
rect = rightHalf;
return leftHalf;
}
/// <summary>
/// Splits the rect into two at the 'width' and returns the right half, out Rect remaining will contain the left half
/// If width is negative it will be split relative to the left edge rather than the right edge
/// </summary>
public static Rect TakeRight(this ref Rect rect, float width, float spacing = 0)
{
var leftHalf = TakeLeft(ref rect, -width, 0);
leftHalf.width -= DefaultSpacing(ref spacing);
var rightHalf = rect;
rect = leftHalf;
return rightHalf;
}
/// <summary>
/// Returns a rect for a single line property and populates bottomHalf with the remaining space
/// </summary>
/// <param name="rect">the rect to take from</param>
/// <param name="bottomHalf">the rect to be overwritten with the remaining space</param>
/// <param name="spacing">an extra space to be removed from the bottom half, when <0 standardVerticalSpacing is used</param>
/// <returns></returns>
public static Rect TakeSingleLine(this ref Rect rect, float spacing = -1)
{
return TakeTop(ref rect, SingleLine, spacing);
}
public static Rect TakeControlLabel(this ref Rect rect)
{
return TakeLeft(ref rect, LabelWidth);
}
/// <summary>
/// Calculates the height of a set of single line properties
/// </summary>
/// <param name="lineCount">the number of single line properties</param>
/// <param name="spacing">(optional) override for spacing between lines, when < 0 standardVerticalSpacing will be used</param>
/// <returns>The height for the properties</returns>
public static float SumSingleLineHeights(int lineCount, float spacing = -1)
{
return lineCount * SingleLine + (lineCount - 1) * DefaultSpacing(ref spacing);
}
/// <summary>
/// Populates contentRect with a rect of the specified height to be used in a scroll rect, returns the orignal rect
/// </summary>
public static Rect ScrollContentY(this Rect rect, float contentHeight, out Rect contentRect)
{
contentRect = new Rect(rect);
contentRect.height = contentHeight;
if (contentHeight > rect.height) contentRect.width -= 14f;
return rect;
}
/// <summary>
/// When the passed value if < 0, returns the standard vertical spacing
/// </summary>
private static float DefaultSpacing(ref float input) => input = (input >= 0 ? input : Spacing);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment