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();
}
}
@frarees
Copy link
Author

frarees commented Mar 3, 2022

Hey @viruseg thanks for the suggestions.

Turns out I had a big rework commited locally since last year. Sadly I didn't get to document the changes, so I will just go ahead and publish it. I think I addressed the mixed value behaviour too, but give it a go and let me know if that's the case.

I've also encapsulated the drawer in a BeginProperty/EndProperty scope 👍

@viruseg
Copy link

viruseg commented Mar 3, 2022

Thank you for the source code. It saved me a lot of time. Fixing these bugs will help everyone else who gets here via google.

The bug with multiple selected GameObjects still remains. For example, by changing min, max will become the same for all GameObjects.
In lines 149 and 170 you need to save the value differently. Something like this:

private static void SavePropertyValue(SerializedProperty property, float value)
{
    switch (property.propertyType)
    {
        case SerializedPropertyType.Float:
            property.floatValue = value;
            break;
        
        case SerializedPropertyType.Integer:
            property.intValue = (int) Math.Round(value, 0);
            break;
    }
}

And there is a bug with negative values for the integer slider. When using the slider, the values are rounded to the wrong side.
[MinMaxSlider(-5, 5)] public Vector2Int test;
Line 47 should be replaced with this
property.vector2IntValue = new Vector2Int((int) Math.Round(min, 0), (int) Math.Round(max, 0));

@frarees
Copy link
Author

frarees commented Mar 4, 2022

@viruseg

there is a bug with negative values for the integer slider.

Nice, I'll get a fix.

The bug with multiple selected GameObjects still remains. For example, by changing min, max will become the same for all GameObjects.

Not sure how far we can go while using EditorGUI.MinMaxSlider. There's no way (that I've found) to tell apart when you're grabbing just one handle. In the past I've tried to store the min and max values on press, and compare every edit to see which value haven't changed, so that we edit only one. But both values can get modified. I feel this is an issue with EditorGUI.MinMaxSlider.

For example, look at the max value on this slider, while I grab the min handle:

example

EDIT: actually, I'm doing some tests now, and comparison through Mathf.Approximately seems to do just fine!

example2

Pushed a fix, give it a go

@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