A game framework written with osu! in mind.
at master 338 lines 14 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Diagnostics; 6using System.Globalization; 7using osu.Framework.Utils; 8 9namespace osu.Framework.Bindables 10{ 11 public class BindableNumber<T> : RangeConstrainedBindable<T>, IBindableNumber<T> 12 where T : struct, IComparable<T>, IConvertible, IEquatable<T> 13 { 14 public event Action<T> PrecisionChanged; 15 16 public BindableNumber(T defaultValue = default) 17 : base(defaultValue) 18 { 19 if (!Validation.IsSupportedBindableNumberType<T>()) 20 { 21 throw new NotSupportedException( 22 $"{nameof(BindableNumber<T>)} only accepts the primitive numeric types (except for {typeof(decimal).FullName}) as type arguments. You provided {typeof(T).FullName}."); 23 } 24 25 precision = DefaultPrecision; 26 27 // Re-apply the current value to apply the default precision value 28 setValue(Value); 29 } 30 31 private T precision; 32 33 public T Precision 34 { 35 get => precision; 36 set 37 { 38 if (precision.Equals(value)) 39 return; 40 41 if (value.CompareTo(default) <= 0) 42 throw new ArgumentOutOfRangeException(nameof(Precision), value, "Must be greater than 0."); 43 44 SetPrecision(value, true, this); 45 } 46 } 47 48 /// <summary> 49 /// Sets the precision. This method does no equality comparisons. 50 /// </summary> 51 /// <param name="precision">The new precision.</param> 52 /// <param name="updateCurrentValue">Whether to update the current value after the precision is set.</param> 53 /// <param name="source">The bindable that triggered this. A null value represents the current bindable instance.</param> 54 internal void SetPrecision(T precision, bool updateCurrentValue, BindableNumber<T> source) 55 { 56 this.precision = precision; 57 TriggerPrecisionChange(source); 58 59 if (updateCurrentValue) 60 { 61 // Re-apply the current value to apply the new precision 62 setValue(Value); 63 } 64 } 65 66 public override T Value 67 { 68 get => base.Value; 69 set => setValue(value); 70 } 71 72 private void setValue(T value) 73 { 74 if (Precision.CompareTo(DefaultPrecision) > 0) 75 { 76 double doubleValue = ClampValue(value, MinValue, MaxValue).ToDouble(NumberFormatInfo.InvariantInfo); 77 doubleValue = Math.Round(doubleValue / Precision.ToDouble(NumberFormatInfo.InvariantInfo)) * Precision.ToDouble(NumberFormatInfo.InvariantInfo); 78 79 base.Value = (T)Convert.ChangeType(doubleValue, typeof(T), CultureInfo.InvariantCulture); 80 } 81 else 82 base.Value = value; 83 } 84 85 protected override T DefaultMinValue 86 { 87 get 88 { 89 Debug.Assert(Validation.IsSupportedBindableNumberType<T>()); 90 91 if (typeof(T) == typeof(sbyte)) 92 return (T)(object)sbyte.MinValue; 93 if (typeof(T) == typeof(byte)) 94 return (T)(object)byte.MinValue; 95 if (typeof(T) == typeof(short)) 96 return (T)(object)short.MinValue; 97 if (typeof(T) == typeof(ushort)) 98 return (T)(object)ushort.MinValue; 99 if (typeof(T) == typeof(int)) 100 return (T)(object)int.MinValue; 101 if (typeof(T) == typeof(uint)) 102 return (T)(object)uint.MinValue; 103 if (typeof(T) == typeof(long)) 104 return (T)(object)long.MinValue; 105 if (typeof(T) == typeof(ulong)) 106 return (T)(object)ulong.MinValue; 107 if (typeof(T) == typeof(float)) 108 return (T)(object)float.MinValue; 109 110 return (T)(object)double.MinValue; 111 } 112 } 113 114 protected override T DefaultMaxValue 115 { 116 get 117 { 118 Debug.Assert(Validation.IsSupportedBindableNumberType<T>()); 119 120 if (typeof(T) == typeof(sbyte)) 121 return (T)(object)sbyte.MaxValue; 122 if (typeof(T) == typeof(byte)) 123 return (T)(object)byte.MaxValue; 124 if (typeof(T) == typeof(short)) 125 return (T)(object)short.MaxValue; 126 if (typeof(T) == typeof(ushort)) 127 return (T)(object)ushort.MaxValue; 128 if (typeof(T) == typeof(int)) 129 return (T)(object)int.MaxValue; 130 if (typeof(T) == typeof(uint)) 131 return (T)(object)uint.MaxValue; 132 if (typeof(T) == typeof(long)) 133 return (T)(object)long.MaxValue; 134 if (typeof(T) == typeof(ulong)) 135 return (T)(object)ulong.MaxValue; 136 if (typeof(T) == typeof(float)) 137 return (T)(object)float.MaxValue; 138 139 return (T)(object)double.MaxValue; 140 } 141 } 142 143 /// <summary> 144 /// The default <see cref="Precision"/>. 145 /// </summary> 146 protected virtual T DefaultPrecision 147 { 148 get 149 { 150 if (typeof(T) == typeof(sbyte)) 151 return (T)(object)(sbyte)1; 152 if (typeof(T) == typeof(byte)) 153 return (T)(object)(byte)1; 154 if (typeof(T) == typeof(short)) 155 return (T)(object)(short)1; 156 if (typeof(T) == typeof(ushort)) 157 return (T)(object)(ushort)1; 158 if (typeof(T) == typeof(int)) 159 return (T)(object)1; 160 if (typeof(T) == typeof(uint)) 161 return (T)(object)1U; 162 if (typeof(T) == typeof(long)) 163 return (T)(object)1L; 164 if (typeof(T) == typeof(ulong)) 165 return (T)(object)1UL; 166 if (typeof(T) == typeof(float)) 167 return (T)(object)float.Epsilon; 168 169 return (T)(object)double.Epsilon; 170 } 171 } 172 173 public override void TriggerChange() 174 { 175 base.TriggerChange(); 176 177 TriggerPrecisionChange(this, false); 178 } 179 180 protected void TriggerPrecisionChange(BindableNumber<T> source = null, bool propagateToBindings = true) 181 { 182 // check a bound bindable hasn't changed the value again (it will fire its own event) 183 T beforePropagation = precision; 184 185 if (propagateToBindings && Bindings != null) 186 { 187 foreach (var b in Bindings) 188 { 189 if (b == source) continue; 190 191 if (b is BindableNumber<T> bn) 192 bn.SetPrecision(precision, false, this); 193 } 194 } 195 196 if (beforePropagation.Equals(precision)) 197 PrecisionChanged?.Invoke(precision); 198 } 199 200 public override void BindTo(Bindable<T> them) 201 { 202 if (them is BindableNumber<T> other) 203 Precision = other.Precision; 204 205 base.BindTo(them); 206 } 207 208 public override void UnbindEvents() 209 { 210 base.UnbindEvents(); 211 212 PrecisionChanged = null; 213 } 214 215 public bool IsInteger => 216 typeof(T) != typeof(float) && 217 typeof(T) != typeof(double); // Will be **constant** after JIT. 218 219 public void Set<TNewValue>(TNewValue val) where TNewValue : struct, 220 IFormattable, IConvertible, IComparable<TNewValue>, IEquatable<TNewValue> 221 { 222 Debug.Assert(Validation.IsSupportedBindableNumberType<T>()); 223 224 // Comparison between typeof(T) and type literals are treated as **constant** on value types. 225 // Code paths for other types will be eliminated. 226 if (typeof(T) == typeof(byte)) 227 ((BindableNumber<byte>)(object)this).Value = val.ToByte(NumberFormatInfo.InvariantInfo); 228 else if (typeof(T) == typeof(sbyte)) 229 ((BindableNumber<sbyte>)(object)this).Value = val.ToSByte(NumberFormatInfo.InvariantInfo); 230 else if (typeof(T) == typeof(ushort)) 231 ((BindableNumber<ushort>)(object)this).Value = val.ToUInt16(NumberFormatInfo.InvariantInfo); 232 else if (typeof(T) == typeof(short)) 233 ((BindableNumber<short>)(object)this).Value = val.ToInt16(NumberFormatInfo.InvariantInfo); 234 else if (typeof(T) == typeof(uint)) 235 ((BindableNumber<uint>)(object)this).Value = val.ToUInt32(NumberFormatInfo.InvariantInfo); 236 else if (typeof(T) == typeof(int)) 237 ((BindableNumber<int>)(object)this).Value = val.ToInt32(NumberFormatInfo.InvariantInfo); 238 else if (typeof(T) == typeof(ulong)) 239 ((BindableNumber<ulong>)(object)this).Value = val.ToUInt64(NumberFormatInfo.InvariantInfo); 240 else if (typeof(T) == typeof(long)) 241 ((BindableNumber<long>)(object)this).Value = val.ToInt64(NumberFormatInfo.InvariantInfo); 242 else if (typeof(T) == typeof(float)) 243 ((BindableNumber<float>)(object)this).Value = val.ToSingle(NumberFormatInfo.InvariantInfo); 244 else if (typeof(T) == typeof(double)) 245 ((BindableNumber<double>)(object)this).Value = val.ToDouble(NumberFormatInfo.InvariantInfo); 246 } 247 248 public void Add<TNewValue>(TNewValue val) where TNewValue : struct, 249 IFormattable, IConvertible, IComparable<TNewValue>, IEquatable<TNewValue> 250 { 251 Debug.Assert(Validation.IsSupportedBindableNumberType<T>()); 252 253 // Comparison between typeof(T) and type literals are treated as **constant** on value types. 254 // Code pathes for other types will be eliminated. 255 if (typeof(T) == typeof(byte)) 256 ((BindableNumber<byte>)(object)this).Value += val.ToByte(NumberFormatInfo.InvariantInfo); 257 else if (typeof(T) == typeof(sbyte)) 258 ((BindableNumber<sbyte>)(object)this).Value += val.ToSByte(NumberFormatInfo.InvariantInfo); 259 else if (typeof(T) == typeof(ushort)) 260 ((BindableNumber<ushort>)(object)this).Value += val.ToUInt16(NumberFormatInfo.InvariantInfo); 261 else if (typeof(T) == typeof(short)) 262 ((BindableNumber<short>)(object)this).Value += val.ToInt16(NumberFormatInfo.InvariantInfo); 263 else if (typeof(T) == typeof(uint)) 264 ((BindableNumber<uint>)(object)this).Value += val.ToUInt32(NumberFormatInfo.InvariantInfo); 265 else if (typeof(T) == typeof(int)) 266 ((BindableNumber<int>)(object)this).Value += val.ToInt32(NumberFormatInfo.InvariantInfo); 267 else if (typeof(T) == typeof(ulong)) 268 ((BindableNumber<ulong>)(object)this).Value += val.ToUInt64(NumberFormatInfo.InvariantInfo); 269 else if (typeof(T) == typeof(long)) 270 ((BindableNumber<long>)(object)this).Value += val.ToInt64(NumberFormatInfo.InvariantInfo); 271 else if (typeof(T) == typeof(float)) 272 ((BindableNumber<float>)(object)this).Value += val.ToSingle(NumberFormatInfo.InvariantInfo); 273 else if (typeof(T) == typeof(double)) 274 ((BindableNumber<double>)(object)this).Value += val.ToDouble(NumberFormatInfo.InvariantInfo); 275 } 276 277 /// <summary> 278 /// Sets the value of the bindable to Min + (Max - Min) * amt 279 /// <param name="amt">The proportional amount to set, ranging from 0 to 1.</param> 280 /// <param name="snap">If greater than 0, snap the final value to the closest multiple of this number.</param> 281 /// </summary> 282 public void SetProportional(float amt, float snap = 0) 283 { 284 var min = MinValue.ToDouble(NumberFormatInfo.InvariantInfo); 285 var max = MaxValue.ToDouble(NumberFormatInfo.InvariantInfo); 286 var value = min + (max - min) * amt; 287 if (snap > 0) 288 value = Math.Round(value / snap) * snap; 289 Set(value); 290 } 291 292 IBindableNumber<T> IBindableNumber<T>.GetBoundCopy() => GetBoundCopy(); 293 294 public new BindableNumber<T> GetBoundCopy() => (BindableNumber<T>)base.GetBoundCopy(); 295 296 public new BindableNumber<T> GetUnboundCopy() => (BindableNumber<T>)base.GetUnboundCopy(); 297 298 public override string ToString() => Value.ToString(NumberFormatInfo.InvariantInfo); 299 300 public override bool IsDefault 301 { 302 get 303 { 304 if (typeof(T) == typeof(double)) 305 { 306 // Take 50% of the precision to ensure the value doesn't underflow and return true for non-default values. 307 return Utils.Precision.AlmostEquals((double)(object)Value, (double)(object)Default, (double)(object)Precision / 2); 308 } 309 310 if (typeof(T) == typeof(float)) 311 { 312 // Take 50% of the precision to ensure the value doesn't underflow and return true for non-default values. 313 return Utils.Precision.AlmostEquals((float)(object)Value, (float)(object)Default, (float)(object)Precision / 2); 314 } 315 316 return base.IsDefault; 317 } 318 } 319 320 protected override Bindable<T> CreateInstance() => new BindableNumber<T>(); 321 322 protected sealed override T ClampValue(T value, T minValue, T maxValue) => max(minValue, min(maxValue, value)); 323 324 protected sealed override bool IsValidRange(T min, T max) => min.CompareTo(max) <= 0; 325 326 private static T max(T value1, T value2) 327 { 328 var comparison = value1.CompareTo(value2); 329 return comparison > 0 ? value1 : value2; 330 } 331 332 private static T min(T value1, T value2) 333 { 334 var comparison = value1.CompareTo(value2); 335 return comparison > 0 ? value2 : value1; 336 } 337 } 338}