A game framework written with osu! in mind.
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}