// 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; } } }