// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved.
// This code can only be used under the standard Unity Asset Store End User License Agreement
// A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Doozy.Editor.EditorUI.Utils;
using Doozy.Editor.Reactor.Internal;
using Doozy.Runtime.Colors;
using Doozy.Runtime.Common.Events;
using Doozy.Runtime.Common.Extensions;
using Doozy.Runtime.Reactor;
using Doozy.Runtime.Reactor.Easings;
using Doozy.Runtime.Reactor.Internal;
using Doozy.Runtime.Reactor.Reactions;
using Doozy.Runtime.UIElements.Extensions;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UIElements;
// ReSharper disable MemberCanBePrivate.Global
namespace Doozy.Editor.EditorUI.Components
{
public class FluidRangeSlider : VisualElement
{
public VisualElement labelsContainer { get; }
public VisualElement snapIntervalIndicatorsContainer { get; }
public VisualElement snapValuesIndicatorsContainer { get; }
public VisualElement sliderContainer { get; }
public Slider slider { get; }
public Label lowValueLabel { get; private set; }
public Label highValueLabel { get; private set; }
public Label valueLabel { get; private set; }
/// If enabled, the slider will snap to the snapInterval value
public bool snapToIntervalIsEnabled { get; set; } = true;
/// Value to which the slider will snap to if snapToIntervalIsEnabled is true
public float snapInterval { get; set; } = 0.1f;
/// If enabled, the slider will snap to the snapValues values
public bool snapToValuesIsEnabled { get; set; } = false;
/// Distance from the snapValues values to which the slider will snap to if snapToValuesIsEnabled is true
public float snapDistanceForSnapValues { get; set; } = 0.1f;
/// Values to which the slider will snap to if snapToValuesIsEnabled is true
public float[] snapValues { get; set; } = { 0.1f, 0.5f, 1f, 2f, 5f, 10f };
/// If enabled, the slider will reset to the autoResetValue when the user releases the slider handle
public bool autoResetToValue { get; set; } = false;
/// Value to which the slider will reset to when the user releases the slider handle if autoResetToValue is true.
public float autoResetValue { get; set; } = 0f;
public UnityEvent onStartValueChange { get; } = new UnityEvent();
public UnityEvent onEndValueChange { get; } = new UnityEvent();
public FloatEvent onValueChanged { get; } = new FloatEvent();
internal FloatReaction setToValueReaction { get; set; }
public VisualElement sliderTracker { get; }
public VisualElement sliderDraggerBorder { get; }
public VisualElement sliderDragger { get; }
private const float TRACKER_OFFSET = 4;
private float sliderTrackerWidth => sliderTracker.resolvedStyle.width - TRACKER_OFFSET * 2;
private float length => slider.highValue - slider.lowValue;
public FluidRangeSlider()
{
this
.SetStyleFlexShrink(0)
.SetStylePaddingLeft(DesignUtils.k_Spacing2X)
.SetStylePaddingRight(DesignUtils.k_Spacing2X)
.RegisterCallback(evt =>
{
UpdateSnapValuesIndicators();
});
labelsContainer =
new VisualElement()
.SetName("Labels Container")
.SetStyleFlexDirection(FlexDirection.Row)
.SetStyleJustifyContent(Justify.Center)
.SetStyleAlignItems(Align.FlexStart)
.SetStyleMarginTop(4)
.SetStyleFlexGrow(1);
sliderContainer =
new VisualElement()
.SetStyleHeight(28)
.SetName("Slider Container")
.SetStyleFlexGrow(1)
.SetStylePaddingTop(8);
snapIntervalIndicatorsContainer =
new VisualElement()
.SetName("Snap Interval Indicators Container")
.SetStylePosition(Position.Absolute)
.SetStyleLeft(0)
.SetStyleTop(0)
.SetStyleRight(0)
.SetStyleBottom(0);
snapValuesIndicatorsContainer =
new VisualElement()
.SetName("Snap Values Indicators Container")
.SetStylePosition(Position.Absolute)
.SetStyleLeft(0)
.SetStyleTop(0)
.SetStyleRight(0)
.SetStyleBottom(0);
slider =
new Slider()
.ResetLayout();
sliderTracker = slider.Q("unity-tracker");
sliderDraggerBorder = slider.Q("unity-dragger-border");
sliderDragger = slider.Q("unity-dragger");
sliderDragger.SetStyleBorderColor(EditorColors.Default.BoxBackground.WithRGBShade(0.4f));
FloatReaction sliderDraggerBorderReaction =
Reaction.Get()
.SetEditorHeartbeat()
.SetGetter(() => sliderDraggerBorder.GetStyleWidth())
.SetSetter(value =>
{
sliderDraggerBorder.SetStyleSize(value);
float positionOffset = 5 - value * 0.25f;
sliderDraggerBorder.SetStyleLeft(positionOffset);
sliderDraggerBorder.SetStyleTop(positionOffset);
})
.SetDuration(0.15f)
.SetEase(Ease.OutSine);
sliderDraggerBorderReaction.SetFrom(0f);
sliderDraggerBorderReaction.SetTo(16f);
sliderDragger.RegisterCallback(evt => sliderDraggerBorderReaction?.Play());
sliderDragger.RegisterCallback(evt => sliderDraggerBorderReaction?.Play(PlayDirection.Reverse));
setToValueReaction =
Reaction
.Get()
.SetEditorHeartbeat()
.SetDuration(0.34f)
.SetEase(Ease.OutExpo)
.SetGetter(() => slider.value)
.SetSetter(value =>
{
slider.SetValueWithoutNotify(value);
if (valueLabel != null)
{
value =
snapToIntervalIsEnabled
? value.RoundToMultiple(snapInterval)
: value.Round(2);
valueLabel.text = value.ToString(CultureInfo.InvariantCulture);
}
});
slider.RegisterValueChangedCallback(evt =>
{
if (evt?.newValue == null) return;
float value = evt.newValue;
bool snappedToValue = false; //flag to check if the value was snapped to a snap value
if (snapToValuesIsEnabled)
{
foreach (float snapValue in snapValues)
if (Math.Abs(snapValue - value) <= snapDistanceForSnapValues)
{
value = snapValue;
snappedToValue = true;
break;
}
}
if (!snappedToValue && snapToIntervalIsEnabled)
{
value = evt.newValue.RoundToMultiple(snapInterval);
}
if (valueLabel != null)
{
valueLabel.text = value.ToString(CultureInfo.InvariantCulture);
}
slider.SetValueWithoutNotify(value);
onValueChanged?.Invoke(value);
});
slider.RegisterCallback(evt =>
{
if (autoResetToValue & !slider.value.CloseTo(autoResetValue, 0.01f))
setToValueReaction?.SetProgressAtOne();
onStartValueChange?.Invoke();
sliderDraggerBorderReaction?.Play(PlayDirection.Forward);
});
slider.RegisterCallback(evt =>
{
onEndValueChange?.Invoke();
sliderDraggerBorderReaction?.Play(PlayDirection.Reverse);
if (!autoResetToValue) return;
setToValueReaction.SetFrom(slider.value);
setToValueReaction.SetTo(autoResetValue);
setToValueReaction.Play();
});
Initialize();
Compose();
}
public FluidRangeSlider(float lowValue, float highValue) : this() =>
this.SetSliderLowAndHighValues(lowValue, highValue);
private void Initialize()
{
Label GetLabel() =>
DesignUtils.fieldLabel
.SetStyleMinWidth(40)
.SetStyleWidth(40)
.SetStyleFontSize(11);
valueLabel =
GetLabel()
.SetStyleColor(EditorColors.Default.UnityThemeInversed)
.SetStyleTextAlign(TextAnchor.LowerCenter)
.SetStyleFontSize(13);
lowValueLabel = GetLabel().SetStyleTextAlign(TextAnchor.UpperLeft);
highValueLabel = GetLabel().SetStyleTextAlign(TextAnchor.UpperRight);
//react to clicks on labels
lowValueLabel.AddManipulator(new Clickable(() => this.SetSliderValue(slider.lowValue)));
highValueLabel.AddManipulator(new Clickable(() => this.SetSliderValue(slider.highValue)));
labelsContainer
.AddChild(lowValueLabel)
.AddChild(DesignUtils.flexibleSpace)
.AddChild(valueLabel)
.AddChild(DesignUtils.flexibleSpace)
.AddChild(highValueLabel);
sliderContainer
.SetStyleHeight(20)
.AddChild(snapIntervalIndicatorsContainer)
.AddChild(snapValuesIndicatorsContainer)
.AddChild(slider);
}
private void Compose()
{
this
.AddChild(sliderContainer)
.AddChild(labelsContainer);
}
///
/// Clean values from snap values that are outside the slider range and sorts them in ascending order.
///
/// The values to clean.
/// The cleaned values.
internal List CleanValues(IEnumerable values)
{
float minValue = Mathf.Min(slider.lowValue, slider.highValue);
float maxValue = Mathf.Max(slider.lowValue, slider.highValue);
var list = values.Where(v => v >= minValue && v <= maxValue).ToList();
list.Sort();
return list;
}
internal void UpdateSnapValuesIndicators()
{
snapValuesIndicatorsContainer.Clear();
if (!snapToValuesIsEnabled) return;
if (float.IsNaN(sliderTrackerWidth)) return;
foreach (float snapValue in CleanValues(snapValues))
{
float normalizedValue = (snapValue - slider.lowValue) / length;
float position = normalizedValue * sliderTrackerWidth + TRACKER_OFFSET;
Label label =
DesignUtils.fieldLabel
.SetText(snapValue.ToString(CultureInfo.InvariantCulture))
.SetStyleMarginBottom(2)
.SetStyleTextAlign(TextAnchor.MiddleCenter)
.SetStyleWidth(30)
.SetStyleMarginLeft(-14);
//react to clicks on labels
label.AddManipulator(new Clickable(() => this.SetSliderValue(snapValue)));
snapValuesIndicatorsContainer
.AddChild
(
new VisualElement()
.SetName($"Snap Value Indicator: {snapValue}")
.SetStylePosition(Position.Absolute)
.SetStyleLeft(position)
.AddChild(label)
.AddChild
(
DesignUtils.dividerVertical
.SetStyleMargins(0)
.SetStyleHeight(8)
)
);
if (snapValue.Approximately(slider.lowValue) || snapValue.Approximately(slider.highValue))
{
label.visible = false;
}
}
}
}
public static class FluidRangeSliderExtensions
{
/// Set an accent color for the slider
/// Target
/// Accent color
public static T SetAccentColor(this T target, Color color) where T : FluidRangeSlider
{
target.valueLabel.SetStyleColor(color);
target.sliderDraggerBorder.SetStyleBackgroundColor(color.WithAlpha(0.2f));
target.sliderDragger.SetStyleBackgroundColor(color);
return target;
}
/// Adds the given callback to the list of callbacks that will be invoked when the slider value changes.
/// Target
/// Callback triggered when the slider value changes.
public static T AddOnValueChangedListener(this T slider, UnityAction callback) where T : FluidRangeSlider
{
slider.onValueChanged.AddListener(callback);
return slider;
}
/// Removes the given callback from the slider's onValueChanged event.
/// Target
/// Callback
public static T RemoveOnValueChangedListener(this T slider, UnityAction callback) where T : FluidRangeSlider
{
slider.onValueChanged.RemoveListener(callback);
return slider;
}
/// Removes all listeners from the slider's onValueChanged event.
/// Target
public static T RemoveAllOnValueChangedListeners(this T slider) where T : FluidRangeSlider
{
slider.onValueChanged.RemoveAllListeners();
return slider;
}
/// Set whether the slider should snap to a snap interval
/// Target
/// Snap to interval
public static T SnapToInterval(this T target, bool enabled) where T : FluidRangeSlider
{
target.snapToIntervalIsEnabled = enabled;
return target;
}
/// Set a snap interval for the slider. Also sets snap to interval as true
/// Target
/// Snap interval value
public static T SetSnapInterval(this T target, float value) where T : FluidRangeSlider
{
target.snapInterval = value;
target.snapToIntervalIsEnabled = true;
return target;
}
/// Set whether the slider should snap to a set of snap values
/// Target
/// Snap to values
public static T SnapToValues(this T target, bool enabled) where T : FluidRangeSlider
{
target.snapToValuesIsEnabled = enabled;
target.UpdateSnapValuesIndicators();
return target;
}
/// Set the snap values for the slider. Also sets snap to values as true
/// Target
/// Snap values
public static T SetSnapValues(this T target, params float[] values) where T : FluidRangeSlider
{
if (values.Length == 0)
{
throw new ArgumentException("Must have at least one snap value");
}
target.snapValues = target.CleanValues(values).ToArray();
target.snapToValuesIsEnabled = true;
target.UpdateSnapValuesIndicators();
return target;
}
/// Set the snap values for the slider
/// Target
/// Snap values interval (how close does the slider value need to be to snap to a snap values value)
/// Target type
public static T SetSnapDistanceForSnapValues(this T target, float snapDistanceForSnapValues) where T : FluidRangeSlider
{
target.snapDistanceForSnapValues = Mathf.Min(0, snapDistanceForSnapValues);
return target;
}
/// Set whether the slider should snap to a set auto reset value
/// Target
/// Snap to auto reset value
public static T SetAutoResetToValue(this T target, bool value) where T : FluidRangeSlider
{
target.autoResetToValue = value;
return target;
}
/// Set the auto reset value for the slider. Also sets auto reset to value as true
/// Target
/// Auto reset value
public static T SetAutoResetValue(this T target, float value) where T : FluidRangeSlider
{
target.autoResetValue = value;
target.autoResetToValue = true;
return target;
}
/// Set the low value of the slider
/// Target
/// New low value
public static T SetSliderLowValue(this T fluidRangeSlider, float lowValue) where T : FluidRangeSlider
{
fluidRangeSlider.slider.lowValue = lowValue;
fluidRangeSlider.lowValueLabel.text = lowValue.ToString(CultureInfo.InvariantCulture);
return fluidRangeSlider;
}
/// Set the high value of the slider
/// Target
/// New high value
public static T SetSliderHighValue(this T fluidRangeSlider, float highValue) where T : FluidRangeSlider
{
fluidRangeSlider.slider.highValue = highValue;
fluidRangeSlider.highValueLabel.text = highValue.ToString(CultureInfo.InvariantCulture);
return fluidRangeSlider;
}
/// Set the low and high values of the slider
/// Target
/// New low value
/// New high value
public static T SetSliderLowAndHighValues(this T fluidRangeSlider, float lowValue, float highValue) where T : FluidRangeSlider
{
return
fluidRangeSlider
.SetSliderLowValue(lowValue)
.SetSliderHighValue(highValue);
}
/// Set a custom text for the low value label
/// Target
/// New text
public static T SetSliderLowValueLabelText(this T fluidRangeSlider, string text) where T : FluidRangeSlider
{
fluidRangeSlider.lowValueLabel.text = text;
return fluidRangeSlider;
}
/// Set a custom text for the high value label
/// Target
/// New text
public static T SetSliderHighValueLabelText(this T fluidRangeSlider, string text) where T : FluidRangeSlider
{
fluidRangeSlider.highValueLabel.text = text;
return fluidRangeSlider;
}
/// Set custom texts for the low and high value labels
/// Target
/// New low value text
public static T SetSliderLowAndHighValueLabelTexts(this T fluidRangeSlider, string lowValueText, string highValueText) where T : FluidRangeSlider =>
fluidRangeSlider
.SetSliderLowValueLabelText(lowValueText)
.SetSliderHighValueLabelText(highValueText);
/// Set the current value of the slider
/// Target
/// New value
/// Should the change be animated?
public static T SetSliderValue(this T fluidRangeSlider, float value, bool animateChange = false) where T : FluidRangeSlider
{
if (animateChange)
{
fluidRangeSlider.setToValueReaction.SetFrom(fluidRangeSlider.slider.value);
fluidRangeSlider.setToValueReaction.SetTo(value);
fluidRangeSlider.setToValueReaction.Play();
return fluidRangeSlider;
}
fluidRangeSlider.slider.value = value;
fluidRangeSlider.valueLabel.text = value.ToString(CultureInfo.InvariantCulture);
return fluidRangeSlider;
}
/// Set the slider's direction
/// Target
/// New slider direction
public static T SetSliderDirection(this T fluidRangeSlider, SliderDirection direction) where T : FluidRangeSlider
{
fluidRangeSlider.slider.direction = direction;
return fluidRangeSlider;
}
}
}