Community Made Tools

Have you made any useful utilities with Odin?

Login and submit your creations here

Toggle Buttons Attribute

Authored by Maciej
Shared 30-09-2021

Attribute that displays bool as toggle buttons (like EnumToggleButtons).

You can provide value or method that returns name, tooltip, icon and color for each button. There is also option to make buttons not have equal width to reduce horizontal size.

Gist version is easier and faster to modify, so use it as it may have newer version than here

Gist

Usage

[ToggleButtons]
public bool m_bool;

[ToggleButtons("On", "Off")]
public bool m_boolWithNames;

[ToggleButtons("@" + nameof(m_trueName), sizeCompensation: 0.2f)]
public bool m_boolWithUnequalSizes;

[ToggleButtons("@" + nameof(m_trueName), trueIcon: "@EditorIcons.Airplane.Raw", falseIcon: "@" + nameof(m_falseIcon)) ]
public bool m_boolWithIcons;

[ToggleButtons(singleButton: true)]
public bool m_boolSingleButton;

[ToggleButtons(trueColor: "@Color.green", falseColor:"@Color.red")]
public bool m_boolColors;

public Texture2D m_falseIcon;
public string m_trueName = "Very true";

ToggleButtonsAttribute.cs

using System;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class ToggleButtonsAttribute : Attribute
{
	public string m_trueText;
	public string m_falseText;
	
	public string m_trueTooltip;
	public string m_falseTooltip;
	
	public string m_trueIcon;
	public string m_falseIcon;

	public string m_trueColor;
	public string m_falseColor;
	
	public float m_sizeCompensationCompensation;
	public bool m_singleButton;

	/// <summary>
	/// Attribute to draw boolean as buttons
	/// </summary>
	/// <param name="trueText">Text for true button. Can be resolved string</param>
	/// <param name="falseText">Text for false button. Can be resolved string</param>
	/// <param name="singleButton">If set to true, only one button matching bool value will be shown</param>
	/// <param name="sizeCompensation">Amount by which smaller button size is lerped to match bigger button.
	/// 0 - original size of smaller button (takes the least space).
	/// 1 - matches size of bigger button.</param>
	/// <param name="trueTooltip">Tooltip for true button. Can be resolved string</param>
	/// <param name="falseTooltip">Tooltip for false button. Can be resolved string</param>
	/// <param name="trueColor">Color of true button</param>
	/// <param name="falseColor">Color of false button</param>
	/// <param name="trueIcon">Icon for true button</param>
	/// <param name="falseIcon">Icon for false button</param>
	public ToggleButtonsAttribute(string trueText = "Yes", string falseText = "No", bool singleButton = false, 
		float sizeCompensation = 1f, string trueTooltip = "", string falseTooltip = "",
		string trueColor = "", string falseColor = "", string trueIcon = "", string falseIcon = "")
	{
		m_trueText = trueText;
		m_falseText = falseText;

		m_singleButton = singleButton;
		m_sizeCompensationCompensation = sizeCompensation;

		m_trueTooltip = trueTooltip;
		m_falseTooltip = falseTooltip;
		
		m_trueIcon = trueIcon;
		m_falseIcon = falseIcon;

		m_trueColor = trueColor;
		m_falseColor = falseColor;
	}
}

**ToggleButtonsAttributeDrawer.cs **

using System;
using System.Linq;
using Sirenix.OdinInspector.Editor;
using Sirenix.OdinInspector.Editor.ValueResolvers;
using Sirenix.Utilities;
using Sirenix.Utilities.Editor;
using UnityEditor;
using UnityEngine;

public class ToggleButtonsAttributeDrawer : OdinAttributeDrawer<ToggleButtonsAttribute>
{
	private static readonly bool DO_MANUAL_COLORING = UnityVersion.IsVersionOrGreater(2019, 3);
	private static readonly Color ACTIVE_COLOR = EditorGUIUtility.isProSkin ? Color.white : new Color(0.802f, 0.802f, 0.802f, 1f);
	private static readonly Color INACTIVE_COLOR = EditorGUIUtility.isProSkin ? new Color(0.75f, 0.75f, 0.75f, 1f) : Color.white;
	
	private Color?[] m_selectionColors;
	private Color? m_color;

	private ValueResolver<string>[] m_nameGetters;
	private ValueResolver<string>[] m_tooltipGetters;
	private ValueResolver<Texture>[] m_iconGetters;
	private ValueResolver<Color>[] m_colorGetters;

	private GUIContent[] m_buttonContents;
	private float[] m_nameSizes;
	private int m_rows = 1;
	private float m_previousControlRectWidth;

	private bool m_needUpdate = false;
	private float m_totalNamesSize = 0f;

	public override bool CanDrawTypeFilter(Type type)
	{
		return type == typeof(bool);
	}

	protected override void Initialize()
	{
		base.Initialize();

		m_nameGetters = new[]
		{
			ValueResolver.GetForString(Property, Attribute.m_trueText),
			ValueResolver.GetForString(Property, Attribute.m_falseText)
		};
		
		m_tooltipGetters = new[]
		{
			ValueResolver.GetForString(Property, Attribute.m_trueTooltip),
			ValueResolver.GetForString(Property, Attribute.m_falseTooltip)
		};
		
		m_iconGetters = new[]
		{
			ValueResolver.Get(Property, Attribute.m_trueIcon, (Texture)null),
			ValueResolver.Get(Property, Attribute.m_falseIcon, (Texture)null)
		};

		m_colorGetters = new[]
		{
			ValueResolver.Get(Property, Attribute.m_trueColor, Color.white),
			ValueResolver.Get(Property, Attribute.m_falseColor, Color.white)
		};

		m_buttonContents = new GUIContent[2];

		for (int i = 0; i < 2; i++)
			m_buttonContents[i] = new GUIContent(m_nameGetters[i].GetValue(), m_iconGetters[i].GetValue(), 
				m_tooltipGetters[i].GetValue());
		 
		m_nameSizes = m_buttonContents.Select(x => SirenixGUIStyles.MiniButtonMid.CalcSize(x).x).ToArray();

		m_rows = 1;
		
		GUIHelper.RequestRepaint();
		
		if (!DO_MANUAL_COLORING)
			return;
		
		m_selectionColors = new Color?[2];
		m_color = new Color?();
	}

	private void UpdateNames()
	{
		UpdateName(0);
		UpdateName(1);
		
		// Add extra padding to smaller button
		if (m_nameSizes[0] > m_nameSizes[1])
			m_nameSizes[1] = Mathf.Lerp(m_nameSizes[1], m_nameSizes[0], Attribute.m_sizeCompensationCompensation);
		else
			m_nameSizes[0] = Mathf.Lerp(m_nameSizes[0], m_nameSizes[1], Attribute.m_sizeCompensationCompensation);

		m_totalNamesSize = m_nameSizes[0] + m_nameSizes[1];
	}

	private void UpdateName(int index)
	{
		var newText = m_nameGetters[index].GetValue();
		var newIcon = m_iconGetters[index].GetValue();
		
		m_buttonContents[index].tooltip = m_tooltipGetters[index].GetValue();

		var needUpdate = m_buttonContents[index].text != newText | m_buttonContents[index].image != newIcon;

		m_needUpdate |= needUpdate;
		
		m_buttonContents[index].text = newText;
		m_buttonContents[index].image = newIcon;
		m_nameSizes[index] = SirenixGUIStyles.MiniButton.CalcSize(m_buttonContents[index]).x;
	}

	protected override void DrawPropertyLayout(GUIContent label)
	{
		foreach (var valueResolver in m_nameGetters) 
			valueResolver.DrawError();
		
		foreach (var valueResolver in m_iconGetters) 
			valueResolver.DrawError();
		
		foreach (var valueResolver in m_tooltipGetters) 
			valueResolver.DrawError();
		
		foreach (var valueResolver in m_colorGetters) 
			valueResolver.DrawError();

		if (Event.current.type == EventType.Layout)
			UpdateNames();
			
		var currentValue = (bool)Property.ValueEntry.WeakSmartValue;

		var buttonIndex = 0;
		
		var rect = new Rect();

		SirenixEditorGUI.GetFeatureRichControlRect(label,
			Mathf.CeilToInt(EditorGUIUtility.singleLineHeight * (Attribute.m_singleButton ? 1 : m_rows)),
			out int _, out bool _, out var valueRect);

		if (Attribute.m_singleButton)
		{
			DrawSingleButton(currentValue, valueRect);
		}
		else
		{
			valueRect.height = EditorGUIUtility.singleLineHeight;
		
			rect = valueRect;
			
			for (int rowIndex = 0; rowIndex < m_rows; ++rowIndex)
			{
				valueRect.xMin = rect.xMin;
				valueRect.xMax = rect.xMax;

				var xMax = valueRect.xMax;

				for (int columnIndex = 0; columnIndex < (m_rows == 2 ? 1 : 2); ++columnIndex)
				{
					valueRect.width = (int)rect.width * m_nameSizes[buttonIndex] / m_totalNamesSize;
					valueRect = DrawButton(buttonIndex, currentValue, valueRect, columnIndex, rowIndex, xMax);
					++buttonIndex;
				}

				valueRect.y += valueRect.height;
			}
		}

		if (Event.current.type != EventType.Repaint || m_previousControlRectWidth == rect.width && !m_needUpdate ||
		    Attribute.m_singleButton)
			return;
		
		m_previousControlRectWidth = rect.width;

		m_rows = rect.width < m_nameSizes[0] + m_nameSizes[1] + 6f ? 2 : 1;

		m_needUpdate = false;
	}

	private void DrawSingleButton(bool currentValue, Rect valueRect)
	{
		if (DO_MANUAL_COLORING)
			m_color = UpdateColor(m_color, currentValue ? ACTIVE_COLOR : INACTIVE_COLOR);

		GUIStyle style = currentValue ? SirenixGUIStyles.MiniButtonSelected : SirenixGUIStyles.MiniButton;
		
		GUI.backgroundColor = m_colorGetters[currentValue ? 0 : 1].GetValue();

		if (DO_MANUAL_COLORING)
			GUIHelper.PushColor(m_color.Value * GUI.color);

		valueRect.x--;
		valueRect.xMax += 2;

		if (GUI.Button(valueRect, m_buttonContents[currentValue ? 0 : 1], style))
			Property.ValueEntry.WeakSmartValue = !currentValue;

		if (DO_MANUAL_COLORING)
			GUIHelper.PopColor();
		
		GUI.backgroundColor = Color.white;
	}

	private Rect DrawButton(int buttonIndex, bool currentValue, Rect valueRect, int columnIndex, int rowIndex,
		float xMax)
	{
		var selectionColor = new Color?();
		var buttonValue = buttonIndex == 0;

		if (DO_MANUAL_COLORING)
		{
			var color = currentValue == buttonValue ? ACTIVE_COLOR : INACTIVE_COLOR;
			selectionColor = m_selectionColors[buttonValue ? 0 : 1];

			selectionColor = UpdateColor(selectionColor, color);

			m_selectionColors[buttonValue ? 0 : 1] = selectionColor;
		}

		var position = valueRect;
		GUIStyle style;

		if (columnIndex == 0 && columnIndex == (m_rows == 2 ? 1 : 2) - 1)
		{
			style = currentValue ? SirenixGUIStyles.MiniButtonSelected : SirenixGUIStyles.MiniButton;
			--position.x;
			position.xMax = xMax + 1f;
		}
		else if (buttonIndex == 0)
			style = currentValue ? SirenixGUIStyles.MiniButtonLeftSelected : SirenixGUIStyles.MiniButtonLeft;
		else
		{
			style = currentValue ? SirenixGUIStyles.MiniButtonRightSelected : SirenixGUIStyles.MiniButtonRight;
			position.xMax = xMax;
		}
		
		GUI.backgroundColor = m_colorGetters[buttonIndex].GetValue();
		
		if (DO_MANUAL_COLORING)
			GUIHelper.PushColor(selectionColor.Value * GUI.color);
		
		if (GUI.Button(position, m_buttonContents[buttonIndex], style))
			Property.ValueEntry.WeakSmartValue = buttonValue;

		GUI.backgroundColor = Color.white;
		
		if (DO_MANUAL_COLORING)
			GUIHelper.PopColor();
		

		valueRect.x += valueRect.width;

		return valueRect;
	}

	private static Color? UpdateColor(Color? nullable, Color color)
	{
		if (!nullable.HasValue)
			nullable = color;
		else if (nullable.Value != color && Event.current.type == EventType.Layout)
		{
			nullable = color.MoveTowards(color, EditorTimeHelper.Time.DeltaTime * 4f);

			GUIHelper.RequestRepaint();
		}

		return nullable;
	}
}