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

Platform: Linux

The float fields are too small again in Unity 2021.2.12f1, even with flexible width.

2022-05-08 MinMaxRange attribute float field size is too small
2022-05-08 MinMaxRange attribute float field width too small even for Vector2Int

The calculation is done in FlexibleFloatFieldWidth and makes sense, but the coefficients are apparently too small. I'm not even on hi-dpi so I don't see why the pixel units scale would have changed...

In addition, while Vector2Int loses numbers after 3 digits, it happens even earlier with Vector2 since we have 2 decimals (at least).
While evaluation the number of decimals may be hard, we could at least at space for 3 characters when using Vector2, and even more when Round = false... I'll try something, but I'm surprised your GIF from Mar 4 looks so good if you're also using a recent version of Unity!

If it's an OS thing and we can prove a difference between Linux, OSX and WIndows with the same code, then I'll send a bug report to Unity about this.

@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