Skip to content

Instantly share code, notes, and snippets.

@frarees
Last active April 4, 2024 12:13
Show Gist options
  • Star 56 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save frarees/9791517 to your computer and use it in GitHub Desktop.
Save frarees/9791517 to your computer and use it in GitHub Desktop.
MinMaxSlider for Unity
// https://frarees.github.io/default-gist-license
using System;
using UnityEngine;
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class MinMaxSliderAttribute : PropertyAttribute
{
public float Min { get; set; }
public float Max { get; set; }
public bool DataFields { get; set; } = true;
public bool FlexibleFields { get; set; } = true;
public bool Bound { get; set; } = true;
public bool Round { get; set; } = true;
public MinMaxSliderAttribute() : this(0, 1)
{
}
public MinMaxSliderAttribute(float min, float max)
{
Min = min;
Max = max;
}
}
// https://frarees.github.io/default-gist-license
using UnityEngine;
using UnityEditor;
[CustomPropertyDrawer(typeof(MinMaxSliderAttribute))]
internal class MinMaxSliderDrawer : PropertyDrawer
{
private const string kVectorMinName = "x";
private const string kVectorMaxName = "y";
private const float kFloatFieldWidth = 16f;
private const float kSpacing = 2f;
private const float kRoundingValue = 100f;
private static readonly int controlHash = "Foldout".GetHashCode();
private static readonly GUIContent unsupported = EditorGUIUtility.TrTextContent("Unsupported field type");
private bool pressed;
private float pressedMin;
private float pressedMax;
private float Round(float value, float roundingValue)
{
return roundingValue == 0 ? value : Mathf.Round(value * roundingValue) / roundingValue;
}
private float FlexibleFloatFieldWidth(float min, float max)
{
var n = Mathf.Max(Mathf.Abs(min), Mathf.Abs(max));
return 14f + (Mathf.Floor(Mathf.Log10(Mathf.Abs(n)) + 1) * 2.5f);
}
private void SetVectorValue(SerializedProperty property, ref float min, ref float max, bool round)
{
if (!pressed || (pressed && !Mathf.Approximately(min, pressedMin)))
{
using (var x = property.FindPropertyRelative(kVectorMinName))
{
SetValue(x, ref min, round);
}
}
if (!pressed || (pressed && !Mathf.Approximately(max, pressedMax)))
{
using (var y = property.FindPropertyRelative(kVectorMaxName))
{
SetValue(y, ref max, round);
}
}
}
private void SetValue(SerializedProperty property, ref float v, bool round)
{
switch (property.propertyType)
{
case SerializedPropertyType.Float:
{
if (round)
{
v = Round(v, kRoundingValue);
}
property.floatValue = v;
}
break;
case SerializedPropertyType.Integer:
{
property.intValue = Mathf.RoundToInt(v);
}
break;
default:
break;
}
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
float min, max;
label = EditorGUI.BeginProperty(position, label, property);
switch (property.propertyType)
{
case SerializedPropertyType.Vector2:
{
var v = property.vector2Value;
min = v.x;
max = v.y;
}
break;
case SerializedPropertyType.Vector2Int:
{
var v = property.vector2IntValue;
min = v.x;
max = v.y;
}
break;
default:
EditorGUI.LabelField(position, label, unsupported);
return;
}
var attr = attribute as MinMaxSliderAttribute;
float ppp = EditorGUIUtility.pixelsPerPoint;
float spacing = kSpacing * ppp;
float fieldWidth = ppp * (attr.DataFields && attr.FlexibleFields ?
FlexibleFloatFieldWidth(attr.Min, attr.Max) :
kFloatFieldWidth);
var indent = EditorGUI.indentLevel;
int id = GUIUtility.GetControlID(controlHash, FocusType.Keyboard, position);
var r = EditorGUI.PrefixLabel(position, id, label);
Rect sliderPos = r;
if (attr.DataFields)
{
sliderPos.x += fieldWidth + spacing;
sliderPos.width -= (fieldWidth + spacing) * 2;
}
if (Event.current.type == EventType.MouseDown &&
sliderPos.Contains(Event.current.mousePosition))
{
pressed = true;
min = Mathf.Clamp(min, attr.Min, attr.Max);
max = Mathf.Clamp(max, attr.Min, attr.Max);
pressedMin = min;
pressedMax = max;
SetVectorValue(property, ref min, ref max, attr.Round);
GUIUtility.keyboardControl = 0; // TODO keep focus but stop editing
}
if (pressed && Event.current.type == EventType.MouseUp)
{
if (attr.Round)
{
SetVectorValue(property, ref min, ref max, true);
}
pressed = false;
}
EditorGUI.BeginChangeCheck();
EditorGUI.indentLevel = 0;
EditorGUI.MinMaxSlider(sliderPos, ref min, ref max, attr.Min, attr.Max);
EditorGUI.indentLevel = indent;
if (EditorGUI.EndChangeCheck())
{
SetVectorValue(property, ref min, ref max, false);
}
if (attr.DataFields)
{
Rect minPos = r;
minPos.width = fieldWidth;
var vectorMinProp = property.FindPropertyRelative(kVectorMinName);
EditorGUI.showMixedValue = vectorMinProp.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
EditorGUI.indentLevel = 0;
min = EditorGUI.DelayedFloatField(minPos, min);
EditorGUI.indentLevel = indent;
if (EditorGUI.EndChangeCheck())
{
if (attr.Bound)
{
min = Mathf.Max(min, attr.Min);
min = Mathf.Min(min, max);
}
SetVectorValue(property, ref min, ref max, attr.Round);
}
vectorMinProp.Dispose();
Rect maxPos = position;
maxPos.x += maxPos.width - fieldWidth;
maxPos.width = fieldWidth;
var vectorMaxProp = property.FindPropertyRelative(kVectorMaxName);
EditorGUI.showMixedValue = vectorMaxProp.hasMultipleDifferentValues;
EditorGUI.BeginChangeCheck();
EditorGUI.indentLevel = 0;
max = EditorGUI.DelayedFloatField(maxPos, max);
EditorGUI.indentLevel = indent;
if (EditorGUI.EndChangeCheck())
{
if (attr.Bound)
{
max = Mathf.Min(max, attr.Max);
max = Mathf.Max(max, min);
}
SetVectorValue(property, ref min, ref max, attr.Round);
}
vectorMaxProp.Dispose();
EditorGUI.showMixedValue = false;
}
EditorGUI.EndProperty();
}
}
@hsandt
Copy link

hsandt commented May 8, 2022

OK, so this is my revised formula:

        private float FlexibleFloatFieldWidth(float min, float max, bool hasDecimals)
        {
            var n = Mathf.Max(Mathf.Abs(min), Mathf.Abs(max));
            float floatFieldWidth = 14f + (Mathf.Floor(Mathf.Log10(Mathf.Abs(n)) + 1) * 8f);
            if (hasDecimals)
            {
                floatFieldWidth += 18f;
            }
            return floatFieldWidth;
        }

8f seems a good estimation of 1 character's max width (I'd rather pass an official constant of max character width, but I don't know where to get that. Note that "1" is thinner, but other digits are about the same width I think.

EDIT: It also takes the possible minus sign into account (if attr.Min >= 0 and Bound = true, you may reduce width a little as there will be no sign).

Then, I added 18f for when the number has decimals, but you can tune that. For instance, you could pass an extra parameter round = attr.Round and if it is false, add even more space to see 4-5 decimals. Unity itself gives a lot of room for floats, so you could copy that.

Finally, change the call to that method to:

FlexibleFloatFieldWidth(attr.Min, attr.Max,
    property.propertyType == SerializedPropertyType.Vector2)

so it detects when decimals can be present.

Here is the result:

2022-05-08 MinMaxRange attribute float field width issue fix - Adjusted parameters

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment