using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Experimental.Rendering; namespace UnityEditor.Rendering { /// <summary> /// Formats the provided descriptor into a linear slider with contextual slider markers, tooltips, and icons. /// </summary> public class LightUnitSlider { /// <summary> /// The <see cref="SerializedObject"/> that contains a <see cref="Light"/> /// </summary> protected SerializedObject m_SerializedObject; static class SliderConfig { public const float k_IconSeparator = 0; public const float k_MarkerWidth = 2; public const float k_MarkerHeight = 2; public const float k_MarkerTooltipScale = 4; public const float k_ThumbTooltipSize = 10; public const float k_KnobSize = 10; } /// <summary> /// The styles to be used on sliders /// </summary> protected static class SliderStyles { /// <summary> A <see cref="GUIStyle"/> with "IconButton" </summary> public static GUIStyle k_IconButton = new GUIStyle("IconButton"); /// <summary> A <see cref="GUIStyle"/> with "ColorPickerSliderBackground" </summary> public static GUIStyle k_TemperatureBorder = new GUIStyle("ColorPickerSliderBackground"); /// <summary> A <see cref="GUIStyle"/> with "ColorPickerHorizThumb" </summary> public static GUIStyle k_TemperatureThumb = new GUIStyle("ColorPickerHorizThumb"); } /// <summary> /// The <see cref="LightUnitSliderUIDescriptor"/> /// </summary> protected readonly LightUnitSliderUIDescriptor m_Descriptor; /// <summary> /// Constructor with a <see cref="LightUnitSliderUIDescriptor"/> /// </summary> /// <param name="descriptor">The <see cref="LightUnitSliderUIDescriptor"/></param> public LightUnitSlider(LightUnitSliderUIDescriptor descriptor) { m_Descriptor = descriptor; } /// <summary> /// Modifies the <see cref="SerializedObject"/> for this Light slider /// </summary> /// <param name="serialized"></param> public void SetSerializedObject(SerializedObject serialized) { m_SerializedObject = serialized; } /// <summary> /// Draws the slider in a given <see cref="Rect"/> /// </summary> /// <param name="rect">The <see cref="Rect"/> to draw the slider into</param> /// <param name="value">The <see cref="SerializedProperty"/> with the property serialized</param> /// <param name="floatValue">The float value modified by the slider GUI</param> public virtual void Draw(Rect rect, SerializedProperty value, ref float floatValue) { BuildRects(rect, out var sliderRect, out var iconRect); if (m_Descriptor.clampValue) ClampValue(ref floatValue, m_Descriptor.sliderRange); var level = CurrentRange(floatValue); DoSlider(sliderRect, ref floatValue, m_Descriptor.sliderRange, level.value); if (m_Descriptor.hasMarkers) { foreach (var r in m_Descriptor.valueRanges) { var markerValue = r.value.y; var markerPosition = GetPositionOnSlider(markerValue, r.value); var markerTooltip = r.content.tooltip; DoSliderMarker(sliderRect, markerPosition, markerValue, markerTooltip); } } var levelIconContent = level.content; var levelRange = level.value; DoIcon(iconRect, levelIconContent, value, floatValue, levelRange.y); var thumbValue = floatValue; var thumbPosition = GetPositionOnSlider(thumbValue, level.value); var thumbTooltip = levelIconContent.tooltip; DoThumbTooltip(sliderRect, thumbPosition, thumbValue, thumbTooltip); } LightUnitSliderUIRange CurrentRange(float value) { foreach (var l in m_Descriptor.valueRanges) { if (value >= l.value.x && value <= l.value.y) { return l; } } var cautionValue = value < m_Descriptor.sliderRange.x ? m_Descriptor.sliderRange.x : m_Descriptor.sliderRange.y; var cautionTooltip = value < m_Descriptor.sliderRange.x ? m_Descriptor.belowRangeTooltip : m_Descriptor.aboveRangeTooltip; return LightUnitSliderUIRange.CautionRange(cautionTooltip, cautionValue); } void BuildRects(Rect baseRect, out Rect sliderRect, out Rect iconRect) { sliderRect = baseRect; sliderRect.width -= EditorGUIUtility.singleLineHeight + SliderConfig.k_IconSeparator; iconRect = baseRect; iconRect.x += sliderRect.width + SliderConfig.k_IconSeparator; iconRect.width = EditorGUIUtility.singleLineHeight; } void ClampValue(ref float value, Vector2 range) => value = Mathf.Clamp(value, range.x, range.y); private static Color k_DarkThemeColor = new Color32(153, 153, 153, 255); private static Color k_LiteThemeColor = new Color32(97, 97, 97, 255); static Color GetMarkerColor() => EditorGUIUtility.isProSkin ? k_DarkThemeColor : k_LiteThemeColor; void DoSliderMarker(Rect rect, float position, float value, string tooltip) { const float width = SliderConfig.k_MarkerWidth; const float height = SliderConfig.k_MarkerHeight; var markerRect = rect; markerRect.width = width; markerRect.height = height; // Vertically align with slider. markerRect.y += (EditorGUIUtility.singleLineHeight / 2f) - 1; // Horizontally place on slider. We need to take into account the "knob" size when doing this, because position 0 and 1 starts // at the center of the knob when it's placed at the left and right corner respectively. We don't do this adjustment when placing // the marker at the corners (to avoid havind the slider slightly extend past the marker) float knobSize = (position > 0f && position < 1f) ? SliderConfig.k_KnobSize : 0f; float start = rect.x + knobSize / 2f; float range = rect.width - knobSize; markerRect.x = start + range * position; // Center the marker on value. const float halfWidth = width * 0.5f; markerRect.x -= halfWidth; // Clamp to the slider edges. float min = rect.x; float max = (rect.x + rect.width) - width; markerRect.x = Mathf.Clamp(markerRect.x, min, max); // Draw marker by manually drawing the rect, and an empty label with the tooltip. EditorGUI.DrawRect(markerRect, GetMarkerColor()); // Scale the marker tooltip for easier discovery const float markerTooltipRectScale = SliderConfig.k_MarkerTooltipScale; var markerTooltipRect = markerRect; markerTooltipRect.width *= markerTooltipRectScale; markerTooltipRect.height *= markerTooltipRectScale; markerTooltipRect.x -= (markerTooltipRect.width * 0.5f) - 1; markerTooltipRect.y -= (markerTooltipRect.height * 0.5f) - 1; EditorGUI.LabelField(markerTooltipRect, GetLightUnitTooltip(tooltip, value, m_Descriptor.unitName)); } void DoIcon(Rect rect, GUIContent icon, SerializedProperty value, float floatValue, float range) { // Draw the context menu feedback before the icon GUI.Box(rect, GUIContent.none, SliderStyles.k_IconButton); var oldColor = GUI.color; GUI.color = Color.clear; EditorGUI.DrawTextureTransparent(rect, icon.image); GUI.color = oldColor; EditorGUI.LabelField(rect, GetLightUnitTooltip(icon.tooltip, range, m_Descriptor.unitName)); // Handle events for context menu var e = Event.current; if (e.type == EventType.MouseDown && e.button == 0) { if (rect.Contains(e.mousePosition)) { var menuPosition = rect.position + rect.size; DoContextMenu(menuPosition, value, floatValue); e.Use(); } } } void DoContextMenu(Vector2 pos, SerializedProperty value, float floatValue) { var menu = new GenericMenu(); foreach (var preset in m_Descriptor.valueRanges) { // Indicate a checkmark if the value is within this preset range. var isInPreset = CurrentRange(floatValue).value == preset.value; menu.AddItem(EditorGUIUtility.TrTextContent(preset.content.tooltip), isInPreset, () => SetValueToPreset(value, preset)); } menu.DropDown(new Rect(pos, Vector2.zero)); } void DoThumbTooltip(Rect rect, float position, float value, string tooltip) { const float size = SliderConfig.k_ThumbTooltipSize; const float halfSize = SliderConfig.k_ThumbTooltipSize * 0.5f; var thumbMarkerRect = rect; thumbMarkerRect.width = size; thumbMarkerRect.height = size; // Vertically align with slider thumbMarkerRect.y += halfSize - 1f; // Horizontally place tooltip on the wheel, thumbMarkerRect.x = rect.x + (rect.width - size) * position; EditorGUI.LabelField(thumbMarkerRect, GetLightUnitTooltip(tooltip, value, m_Descriptor.unitName)); } /// <summary> /// The serialized property for color temperature is stored in the build-in light editor, and we need to use this object to apply the update. /// </summary> /// <param name="value">The value to update</param> /// <param name="preset">The preset range</param> protected virtual void SetValueToPreset(SerializedProperty value, LightUnitSliderUIRange preset) { m_SerializedObject?.Update(); // Set the value to the average of the preset range. value.floatValue = preset.presetValue; m_SerializedObject?.ApplyModifiedProperties(); } /// <summary> /// Gets the tooltip /// </summary> /// <param name="baseTooltip">The base tooltip</param> /// <param name="value">The value</param> /// <param name="unit">The units</param> /// <returns>A well formed tooltip on a <see cref="GUIContent"/></returns> protected virtual GUIContent GetLightUnitTooltip(string baseTooltip, float value, string unit) { var formatValue = value < 100 ? $"{value:n}" : $"{value:n0}"; var tooltip = $"{baseTooltip} | {formatValue} {unit}"; return new GUIContent(string.Empty, tooltip); } /// <summary> /// Draws the slider /// </summary> /// <param name="rect">The <see cref="Rect"/> to draw the slider.</param> /// <param name="value">The current value, and also returns the modified value.</param> /// <param name="sliderRange">The ranges of the slider.</param> /// <param name="_">Not used</param> protected virtual void DoSlider(Rect rect, ref float value, Vector2 sliderRange, Vector2 _) { DoSlider(rect, ref value, sliderRange); } /// <summary> /// Draws a linear slider mapped to the min/max value range. Override this for different slider behavior (texture background, power). /// </summary> /// <param name="rect">The <see cref="Rect"/> to draw the slider.</param> /// <param name="value">The current value, and also returns the modified value.</param> /// <param name="sliderRange">The ranges of the slider.</param> protected virtual void DoSlider(Rect rect, ref float value, Vector2 sliderRange) { value = GUI.HorizontalSlider(rect, value, sliderRange.x, sliderRange.y); } // Remaps value in the domain { Min0, Max0 } to { Min1, Max1 } (by default, normalizes it to (0, 1). static float Remap(float v, float x0, float y0, float x1 = 0f, float y1 = 1f) => x1 + (v - x0) * (y1 - x1) / (y0 - x0); /// <summary> /// Maps a light unit value onto the slider. Keeps in sync placement of markers and tooltips with the slider power. /// Override this in case of non-linear slider. /// </summary> /// <param name="value">The value to get the position at</param> /// <param name="valueRange">The ranges of the values</param> /// <returns>The position</returns> protected virtual float GetPositionOnSlider(float value, Vector2 valueRange) { return GetPositionOnSlider(value); } /// <summary> /// Maps a light unit value onto the slider. Keeps in sync placement of markers and tooltips with the slider power. /// Override this in case of non-linear slider. /// </summary> /// <param name="value">The value to get the position</param> /// <returns>The position on the slider</returns> protected virtual float GetPositionOnSlider(float value) { return Remap(value, m_Descriptor.sliderRange.x, m_Descriptor.sliderRange.y); } } }