Skip to content

Instantly share code, notes, and snippets.

@VictorHHT
Last active March 5, 2023 13:11
Show Gist options
  • Save VictorHHT/24e6c3d8e68eb842a024482ca9ea98b0 to your computer and use it in GitHub Desktop.
Save VictorHHT/24e6c3d8e68eb842a024482ca9ea98b0 to your computer and use it in GitHub Desktop.
Unity MinMaxSlider Property Attribute, with carefully crafted OnGUI drawing to achieve a similar interaction logic like the piano key velocity MinMaxSlider in Apple's GarageBand
using System;
using UnityEditor;
using UnityEngine;
namespace Victor.Tools
{
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public sealed class VTMinMaxSlider : PropertyAttribute
{
internal Vector2 m_MinmaxLimit;
internal Vector2 m_MinmaxValue;
public VTMinMaxSlider(float minLimit, float maxLimit)
{
m_MinmaxLimit.x = minLimit;
m_MinmaxLimit.y = maxLimit;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(VTMinMaxSlider))]
internal sealed class MinMaxSliderDrawer : PropertyDrawer
{
float minValue;
float maxValue;
float minLimit;
float maxLimit;
bool maxReachFlag;
bool minReachFlag;
float minValueAtReached;
float maxValueAtReached;
float mouseXPosAtRached;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
Event e = Event.current;
if (property.propertyType != SerializedPropertyType.Vector2)
{
throw new ArgumentException("VTMinMaxSlider underlying value type must be vector2");
}
var minmaxAttribute = (VTMinMaxSlider)base.attribute;
minLimit = minmaxAttribute.m_MinmaxLimit.x;
maxLimit = minmaxAttribute.m_MinmaxLimit.y;
if (minLimit >= maxLimit)
{
throw new ArgumentOutOfRangeException("VTMinMaxSlider min limit can not greater or equal to max limit");
}
// Make sure the min and max value set by the underlying field doesn't less than min limit,
// and maxValue doesn't exceeds max limit
minValue = Mathf.Max(property.vector2Value.x, minLimit);
maxValue = Mathf.Min(Mathf.Max(property.vector2Value.y, minLimit), maxLimit);
float padding = 2;
EditorGUI.PrefixLabel(position, new GUIContent(property.name));
// Each int field occupies 1/5 of the remaining rect area
float intFieldWidth = ((position.xMax - position.xMin) - EditorGUIUtility.labelWidth - padding * 3) / 5;
// We immediately Clamp its width to make sure it doesn't get too small
intFieldWidth = Mathf.Max(intFieldWidth, 30);
EditorGUI.BeginChangeCheck();
Rect minFieldRect = new Rect(padding + position.x + EditorGUIUtility.labelWidth, position.y, intFieldWidth, position.height);
// We want a delayed field because we don't want to change another bound value immediately when we type a number
minValue = EditorGUI.DelayedFloatField(minFieldRect, minValue);
// The three paddings are min rect padding, padding of itself, max rect padding
float sliderWidth = position.width - intFieldWidth * 2 - EditorGUIUtility.labelWidth - padding * 3;
Rect sliderRect = new Rect(minFieldRect.position.x + minFieldRect.width + padding, position.y, sliderWidth, position.height);
// If user starts to drag the slider, we clear the current keyboard focus
if (e.type == EventType.MouseDown && e.button == 0 && sliderRect.Contains(e.mousePosition))
{
GUIUtility.keyboardControl = 0;
}
if (e.type == EventType.MouseDrag && e.button == 0)
{
if (maxValue >= maxLimit)
{
// To make sure the mouse x position at reached only set once
if (maxReachFlag == false)
{
maxReachFlag = true;
// When max is reached, we set reach min to false
minReachFlag = false;
mouseXPosAtRached = e.mousePosition.x;
minValueAtReached = minValue;
}
if (e.mousePosition.x >= mouseXPosAtRached)
{
// Shrink to the right
float totalRange = maxLimit - minLimit;
// Take slider width into account
float minValueIncreaseAmount = (e.mousePosition.x - mouseXPosAtRached) / sliderWidth * totalRange;
minValue = minValueAtReached + minValueIncreaseAmount;
}
else
{
// To prevent number error
minValue = minValueAtReached;
}
}
else if (minValue <= minLimit)
{
// To make sure the mouse x position at reached only set once
if (minReachFlag == false)
{
minReachFlag = true;
// Vice versa
maxReachFlag = false;
mouseXPosAtRached = e.mousePosition.x;
maxValueAtReached = maxValue;
}
if (e.mousePosition.x < mouseXPosAtRached)
{
// Shrink to the left
float totalRange = maxLimit - minLimit;
float maxValueDecreaseAmount = (mouseXPosAtRached - e.mousePosition.x) / sliderWidth * totalRange;
maxValue = maxValueAtReached - maxValueDecreaseAmount;
}
else
{
// To prevent number error
maxValue = maxValueAtReached;
}
}
}
if (e.type == EventType.MouseUp && e.button == 0)
{
maxReachFlag = false;
minReachFlag = false;
mouseXPosAtRached = 0;
minValueAtReached = 0;
maxValueAtReached = 0;
}
EditorGUI.MinMaxSlider(sliderRect, ref minValue, ref maxValue, minLimit, maxLimit);
Rect maxFieldRect = new Rect(sliderRect.position.x + sliderRect.width + padding, position.y, intFieldWidth, position.height);
maxValue = EditorGUI.DelayedFloatField(maxFieldRect, maxValue);
if (EditorGUI.EndChangeCheck())
{
//Clamp min value between min limit and max value
minValue = Mathf.Clamp(minValue, minLimit, maxValue);
//Clamp max value between min value and max limit
maxValue = Mathf.Clamp(maxValue, minValue, maxLimit);
minmaxAttribute.m_MinmaxValue.x = (float)Math.Round(minValue, 1);
minmaxAttribute.m_MinmaxValue.y = (float)Math.Round(maxValue, 1);
//Assign underlying variable value
property.vector2Value = minmaxAttribute.m_MinmaxValue;
}
}
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment