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

AShim3D commented Jul 20, 2015

Nice

@SamuelKnox
Copy link

I am new to property drawers. What is the usage of this?

@andrew-raphael-lukasik
Copy link

Thanks, very useful

@wilderic
Copy link

Thanks for this script!
Change MinMaxSliderDrawer.cs line 15 to EditorGUI.MinMaxSlider(position, label, ref min, ref max, attr.min, attr.max); to fix deprecated warning in later Unity versions (I'm using 5.5).

@Vilyx
Copy link

Vilyx commented Apr 6, 2018

@frarees
Copy link
Author

frarees commented Jan 25, 2019

Hello everyone! Thanks for giving this little snippet a try. I didn't know this would draw any attention at all... but as it did, I felt it could use a little update. I've updated the files with a bunch of improvements:

  • Changed coding conventions to fit Unity's
  • Inspired by @Vilyx, there's now support for float fields
  • Support for mixed values
  • Fixed issue when the field wasn't a Vector2
  • Proper AttributeUsage
  • Default constructor for normalized ranges
  • Updated deprecated code, thanks @wilderic
  • Tested on Unity 2018

MinMaxSlider Property Drawer Usage

  • Place MinMaxSliderDrawer.cs inside an Editor folder (e.g. Assets/Editor, Assets/Third Parties/Editor, ...)
  • Place MinMaxSliderAttribute.cs inside a non-editor folder (e.g. Assets/, Assets/Scripts, ...)
  • Mark a Vector2 field on your MonoBehaviour or serializable class with [MinMaxSlider] (for 0-1 range) or [MinMaxSlider(<min>, <max>)] (where <min> and <max> are float values)
  • Enjoy your new handsome property drawer!

@exzizt
Copy link

exzizt commented Aug 11, 2019

This is great! My only complaint is that the value boxes are too small to see the entire value in:
image

I may see if I can fix that.

Edit:

Changing const float kFloatFieldWidth = 30f; to const float kFloatFieldWidth = 60f; did the trick enough for me!
image

@Erikoinen
Copy link

A big thank you!

@FishOfTheNorthStar
Copy link

Great script, thanks very much. It had some problems with indent level when used within foldouts, I hacked a little fix in for that and posted it to a fork if anyone else needs it.

@frarees
Copy link
Author

frarees commented Apr 30, 2020

Hi @FishOfTheNorthStar, thanks for your contribution. I'm glad you're finding this little script useful.

I've taken a look at how I could best get around indentation, and worked my way through it. Could you grab the updated gist and see if it works OK for you now?

I took some extra time to improve several aspects of the script:

  • Handle indentation
  • Consistent rect layouting
  • Consistent value fields on retina displays
  • Consistency checks on defined bounds
  • Make sure min <= max
  • Round floats down to 2 decimal places (consist with how Unity handles float fields)
  • Support for Vector2Int
  • Clearer error when using on unsupported types
  • Tried to stick to the eighty column rule

Here's how it looks in Unity 2019.3
preview

@frarees
Copy link
Author

frarees commented May 1, 2020

Added license information.

@hyakugei
Copy link

Added an unbounded property to allow for arbitrary setting of values, unbounded by min/max, in my branch. Thanks for this!

@frarees
Copy link
Author

frarees commented May 13, 2021

Hey everyone! Extra round of maintenance on this property drawer. This time I didn't keep a formal changelog, sorry about that. But there was a lot going on for this update, emphasizing stability and flexibility. Also, added support for unbounded sliders based on @hyakugei's idea.

Overall, with this update the attribute is way more flexible, providing a number of parameters to fine-tune your MinMaxSlider needs:

  • DataFields: Should it draw Min and Max input fields?
  • FlexibleFields: Should the data fields adapt to how long numbers can get? (e.g. [0, 9] shorter number representation than [10, 100])
  • Bound: Are the data fields bound to [Min, Max]? Or are users able to override these with custom values?
  • Round: Should it round to two decimals?

Tested on Unity 2020.3.5f1.

@viruseg
Copy link

viruseg commented Mar 2, 2022

To support Prefab, add MinMaxSliderDrawer.cs to line 58:
EditorGUI.BeginProperty(position, label, property);

And after line 172:
EditorGUI.EndProperty();

This will allow you to track changes to the prefab.

There is also a bug in the code when editing multiple GameObjects. For example, by changing min, max will become the same for all GameObjects.
To fix this bug you need to set a new value in lines 147 and 168 with prop.floatValue = value.
Approximately like this:
vectorMinProp.floatValue = min;

@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