// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Diagnostics; using System.Globalization; using osu.Framework.Utils; namespace osu.Framework.Bindables { public class BindableNumber : RangeConstrainedBindable, IBindableNumber where T : struct, IComparable, IConvertible, IEquatable { public event Action PrecisionChanged; public BindableNumber(T defaultValue = default) : base(defaultValue) { if (!Validation.IsSupportedBindableNumberType()) { throw new NotSupportedException( $"{nameof(BindableNumber)} only accepts the primitive numeric types (except for {typeof(decimal).FullName}) as type arguments. You provided {typeof(T).FullName}."); } precision = DefaultPrecision; // Re-apply the current value to apply the default precision value setValue(Value); } private T precision; public T Precision { get => precision; set { if (precision.Equals(value)) return; if (value.CompareTo(default) <= 0) throw new ArgumentOutOfRangeException(nameof(Precision), value, "Must be greater than 0."); SetPrecision(value, true, this); } } /// /// Sets the precision. This method does no equality comparisons. /// /// The new precision. /// Whether to update the current value after the precision is set. /// The bindable that triggered this. A null value represents the current bindable instance. internal void SetPrecision(T precision, bool updateCurrentValue, BindableNumber source) { this.precision = precision; TriggerPrecisionChange(source); if (updateCurrentValue) { // Re-apply the current value to apply the new precision setValue(Value); } } public override T Value { get => base.Value; set => setValue(value); } private void setValue(T value) { if (Precision.CompareTo(DefaultPrecision) > 0) { double doubleValue = ClampValue(value, MinValue, MaxValue).ToDouble(NumberFormatInfo.InvariantInfo); doubleValue = Math.Round(doubleValue / Precision.ToDouble(NumberFormatInfo.InvariantInfo)) * Precision.ToDouble(NumberFormatInfo.InvariantInfo); base.Value = (T)Convert.ChangeType(doubleValue, typeof(T), CultureInfo.InvariantCulture); } else base.Value = value; } protected override T DefaultMinValue { get { Debug.Assert(Validation.IsSupportedBindableNumberType()); if (typeof(T) == typeof(sbyte)) return (T)(object)sbyte.MinValue; if (typeof(T) == typeof(byte)) return (T)(object)byte.MinValue; if (typeof(T) == typeof(short)) return (T)(object)short.MinValue; if (typeof(T) == typeof(ushort)) return (T)(object)ushort.MinValue; if (typeof(T) == typeof(int)) return (T)(object)int.MinValue; if (typeof(T) == typeof(uint)) return (T)(object)uint.MinValue; if (typeof(T) == typeof(long)) return (T)(object)long.MinValue; if (typeof(T) == typeof(ulong)) return (T)(object)ulong.MinValue; if (typeof(T) == typeof(float)) return (T)(object)float.MinValue; return (T)(object)double.MinValue; } } protected override T DefaultMaxValue { get { Debug.Assert(Validation.IsSupportedBindableNumberType()); if (typeof(T) == typeof(sbyte)) return (T)(object)sbyte.MaxValue; if (typeof(T) == typeof(byte)) return (T)(object)byte.MaxValue; if (typeof(T) == typeof(short)) return (T)(object)short.MaxValue; if (typeof(T) == typeof(ushort)) return (T)(object)ushort.MaxValue; if (typeof(T) == typeof(int)) return (T)(object)int.MaxValue; if (typeof(T) == typeof(uint)) return (T)(object)uint.MaxValue; if (typeof(T) == typeof(long)) return (T)(object)long.MaxValue; if (typeof(T) == typeof(ulong)) return (T)(object)ulong.MaxValue; if (typeof(T) == typeof(float)) return (T)(object)float.MaxValue; return (T)(object)double.MaxValue; } } /// /// The default . /// protected virtual T DefaultPrecision { get { if (typeof(T) == typeof(sbyte)) return (T)(object)(sbyte)1; if (typeof(T) == typeof(byte)) return (T)(object)(byte)1; if (typeof(T) == typeof(short)) return (T)(object)(short)1; if (typeof(T) == typeof(ushort)) return (T)(object)(ushort)1; if (typeof(T) == typeof(int)) return (T)(object)1; if (typeof(T) == typeof(uint)) return (T)(object)1U; if (typeof(T) == typeof(long)) return (T)(object)1L; if (typeof(T) == typeof(ulong)) return (T)(object)1UL; if (typeof(T) == typeof(float)) return (T)(object)float.Epsilon; return (T)(object)double.Epsilon; } } public override void TriggerChange() { base.TriggerChange(); TriggerPrecisionChange(this, false); } protected void TriggerPrecisionChange(BindableNumber source = null, bool propagateToBindings = true) { // check a bound bindable hasn't changed the value again (it will fire its own event) T beforePropagation = precision; if (propagateToBindings && Bindings != null) { foreach (var b in Bindings) { if (b == source) continue; if (b is BindableNumber bn) bn.SetPrecision(precision, false, this); } } if (beforePropagation.Equals(precision)) PrecisionChanged?.Invoke(precision); } public override void BindTo(Bindable them) { if (them is BindableNumber other) Precision = other.Precision; base.BindTo(them); } public override void UnbindEvents() { base.UnbindEvents(); PrecisionChanged = null; } public bool IsInteger => typeof(T) != typeof(float) && typeof(T) != typeof(double); // Will be **constant** after JIT. public void Set(TNewValue val) where TNewValue : struct, IFormattable, IConvertible, IComparable, IEquatable { Debug.Assert(Validation.IsSupportedBindableNumberType()); // Comparison between typeof(T) and type literals are treated as **constant** on value types. // Code paths for other types will be eliminated. if (typeof(T) == typeof(byte)) ((BindableNumber)(object)this).Value = val.ToByte(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(sbyte)) ((BindableNumber)(object)this).Value = val.ToSByte(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(ushort)) ((BindableNumber)(object)this).Value = val.ToUInt16(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(short)) ((BindableNumber)(object)this).Value = val.ToInt16(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(uint)) ((BindableNumber)(object)this).Value = val.ToUInt32(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(int)) ((BindableNumber)(object)this).Value = val.ToInt32(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(ulong)) ((BindableNumber)(object)this).Value = val.ToUInt64(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(long)) ((BindableNumber)(object)this).Value = val.ToInt64(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(float)) ((BindableNumber)(object)this).Value = val.ToSingle(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(double)) ((BindableNumber)(object)this).Value = val.ToDouble(NumberFormatInfo.InvariantInfo); } public void Add(TNewValue val) where TNewValue : struct, IFormattable, IConvertible, IComparable, IEquatable { Debug.Assert(Validation.IsSupportedBindableNumberType()); // Comparison between typeof(T) and type literals are treated as **constant** on value types. // Code pathes for other types will be eliminated. if (typeof(T) == typeof(byte)) ((BindableNumber)(object)this).Value += val.ToByte(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(sbyte)) ((BindableNumber)(object)this).Value += val.ToSByte(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(ushort)) ((BindableNumber)(object)this).Value += val.ToUInt16(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(short)) ((BindableNumber)(object)this).Value += val.ToInt16(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(uint)) ((BindableNumber)(object)this).Value += val.ToUInt32(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(int)) ((BindableNumber)(object)this).Value += val.ToInt32(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(ulong)) ((BindableNumber)(object)this).Value += val.ToUInt64(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(long)) ((BindableNumber)(object)this).Value += val.ToInt64(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(float)) ((BindableNumber)(object)this).Value += val.ToSingle(NumberFormatInfo.InvariantInfo); else if (typeof(T) == typeof(double)) ((BindableNumber)(object)this).Value += val.ToDouble(NumberFormatInfo.InvariantInfo); } /// /// Sets the value of the bindable to Min + (Max - Min) * amt /// The proportional amount to set, ranging from 0 to 1. /// If greater than 0, snap the final value to the closest multiple of this number. /// public void SetProportional(float amt, float snap = 0) { var min = MinValue.ToDouble(NumberFormatInfo.InvariantInfo); var max = MaxValue.ToDouble(NumberFormatInfo.InvariantInfo); var value = min + (max - min) * amt; if (snap > 0) value = Math.Round(value / snap) * snap; Set(value); } IBindableNumber IBindableNumber.GetBoundCopy() => GetBoundCopy(); public new BindableNumber GetBoundCopy() => (BindableNumber)base.GetBoundCopy(); public new BindableNumber GetUnboundCopy() => (BindableNumber)base.GetUnboundCopy(); public override string ToString() => Value.ToString(NumberFormatInfo.InvariantInfo); public override bool IsDefault { get { if (typeof(T) == typeof(double)) { // Take 50% of the precision to ensure the value doesn't underflow and return true for non-default values. return Utils.Precision.AlmostEquals((double)(object)Value, (double)(object)Default, (double)(object)Precision / 2); } if (typeof(T) == typeof(float)) { // Take 50% of the precision to ensure the value doesn't underflow and return true for non-default values. return Utils.Precision.AlmostEquals((float)(object)Value, (float)(object)Default, (float)(object)Precision / 2); } return base.IsDefault; } } protected override Bindable CreateInstance() => new BindableNumber(); protected sealed override T ClampValue(T value, T minValue, T maxValue) => max(minValue, min(maxValue, value)); protected sealed override bool IsValidRange(T min, T max) => min.CompareTo(max) <= 0; private static T max(T value1, T value2) { var comparison = value1.CompareTo(value2); return comparison > 0 ? value1 : value2; } private static T min(T value1, T value2) { var comparison = value1.CompareTo(value2); return comparison > 0 ? value2 : value1; } } }