diff --git a/osu.Framework.Tests/Visual/Bindables/TestSceneBindableNumbers.cs b/osu.Framework.Tests/Visual/Bindables/TestSceneBindableNumbers.cs index c2542cfdca..92efd7e078 100644 --- a/osu.Framework.Tests/Visual/Bindables/TestSceneBindableNumbers.cs +++ b/osu.Framework.Tests/Visual/Bindables/TestSceneBindableNumbers.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Numerics; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -167,45 +168,45 @@ private void testFractionalPrecision() private bool checkExact(decimal value) => checkExact(value, value); private bool checkExact(decimal floatValue, decimal intValue) - => bindableInt.Value == Convert.ToInt32(intValue) - && bindableLong.Value == Convert.ToInt64(intValue) - && bindableFloat.Value == Convert.ToSingle(floatValue) - && bindableDouble.Value == Convert.ToDouble(floatValue); + => bindableInt.Value == (int)intValue + && bindableLong.Value == (long)intValue + && bindableFloat.Value == (float)floatValue + && bindableDouble.Value == (double)floatValue; - private void setMin(T value) + private void setMin(T value) where T : INumber { - bindableInt.MinValue = Convert.ToInt32(value); - bindableLong.MinValue = Convert.ToInt64(value); - bindableFloat.MinValue = Convert.ToSingle(value); - bindableDouble.MinValue = Convert.ToDouble(value); + bindableInt.MinValue = int.CreateTruncating(value); + bindableLong.MinValue = long.CreateTruncating(value); + bindableFloat.MinValue = float.CreateTruncating(value); + bindableDouble.MinValue = double.CreateTruncating(value); } - private void setMax(T value) + private void setMax(T value) where T : INumber { - bindableInt.MaxValue = Convert.ToInt32(value); - bindableLong.MaxValue = Convert.ToInt64(value); - bindableFloat.MaxValue = Convert.ToSingle(value); - bindableDouble.MaxValue = Convert.ToDouble(value); + bindableInt.MaxValue = int.CreateTruncating(value); + bindableLong.MaxValue = long.CreateTruncating(value); + bindableFloat.MaxValue = float.CreateTruncating(value); + bindableDouble.MaxValue = double.CreateTruncating(value); } - private void setValue(T value) + private void setValue(T value) where T : INumber { - bindableInt.Value = Convert.ToInt32(value); - bindableLong.Value = Convert.ToInt64(value); - bindableFloat.Value = Convert.ToSingle(value); - bindableDouble.Value = Convert.ToDouble(value); + bindableInt.Value = int.CreateTruncating(value); + bindableLong.Value = long.CreateTruncating(value); + bindableFloat.Value = float.CreateTruncating(value); + bindableDouble.Value = double.CreateTruncating(value); } - private void setPrecision(T precision) + private void setPrecision(T precision) where T : INumber { - bindableInt.Precision = Convert.ToInt32(precision); - bindableLong.Precision = Convert.ToInt64(precision); - bindableFloat.Precision = Convert.ToSingle(precision); - bindableDouble.Precision = Convert.ToDouble(precision); + bindableInt.Precision = int.CreateTruncating(precision); + bindableLong.Precision = long.CreateTruncating(precision); + bindableFloat.Precision = float.CreateTruncating(precision); + bindableDouble.Precision = double.CreateTruncating(precision); } private partial class BindableDisplayContainer : CompositeDrawable - where T : struct, IComparable, IConvertible, IEquatable + where T : struct, INumber, IMinMaxValue, IConvertible { public BindableDisplayContainer(BindableNumber bindable) { diff --git a/osu.Framework/Bindables/BindableNumber.cs b/osu.Framework/Bindables/BindableNumber.cs index e0fd88febf..c7099dcac7 100644 --- a/osu.Framework/Bindables/BindableNumber.cs +++ b/osu.Framework/Bindables/BindableNumber.cs @@ -4,16 +4,14 @@ #nullable disable using System; -using System.Diagnostics; -using System.Globalization; +using System.Numerics; using JetBrains.Annotations; -using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Utils; namespace osu.Framework.Bindables { public class BindableNumber : RangeConstrainedBindable, IBindableNumber - where T : struct, IComparable, IConvertible, IEquatable + where T : struct, INumber, IMinMaxValue { [CanBeNull] public event Action PrecisionChanged; @@ -40,10 +38,10 @@ public T Precision get => precision; set { - if (precision.Equals(value)) + if (precision == value) return; - if (value.CompareTo(default) <= 0) + if (value <= T.Zero) throw new ArgumentOutOfRangeException(nameof(Precision), value, "Must be greater than 0."); SetPrecision(value, true, this); @@ -76,102 +74,22 @@ public override T Value private void setValue(T value) { - if (Precision.CompareTo(DefaultPrecision) > 0) + if (Precision > DefaultPrecision) { // this rounding is purposefully performed on `decimal` to ensure that the resulting value is the closest possible floating-point // number to actual real-world base-10 decimals, as that is the most common usage of precision. - decimal accurateResult = ClampValue(value, MinValue, MaxValue).ToDecimal(NumberFormatInfo.InvariantInfo); - accurateResult = Math.Round(accurateResult / Precision.ToDecimal(NumberFormatInfo.InvariantInfo)) * Precision.ToDecimal(NumberFormatInfo.InvariantInfo); + decimal accurateResult = decimal.CreateTruncating(T.Clamp(value, MinValue, MaxValue)); + accurateResult = Math.Round(accurateResult / decimal.CreateTruncating(Precision)) * decimal.CreateTruncating(Precision); - base.Value = convertFromDecimal(accurateResult); + base.Value = T.CreateTruncating(accurateResult); } else base.Value = value; } - private T convertFromDecimal(decimal value) - { - if (typeof(T) == typeof(sbyte)) - return (T)(object)Convert.ToSByte(value); - if (typeof(T) == typeof(byte)) - return (T)(object)Convert.ToByte(value); - if (typeof(T) == typeof(short)) - return (T)(object)Convert.ToInt16(value); - if (typeof(T) == typeof(ushort)) - return (T)(object)Convert.ToUInt16(value); - if (typeof(T) == typeof(int)) - return (T)(object)Convert.ToInt32(value); - if (typeof(T) == typeof(uint)) - return (T)(object)Convert.ToUInt32(value); - if (typeof(T) == typeof(long)) - return (T)(object)Convert.ToInt64(value); - if (typeof(T) == typeof(ulong)) - return (T)(object)Convert.ToUInt64(value); - if (typeof(T) == typeof(float)) - return (T)(object)Convert.ToSingle(value); - if (typeof(T) == typeof(double)) - return (T)(object)Convert.ToDouble(value); - - throw new InvalidCastException($"Cannot convert from decimal to {typeof(T).ReadableName()}"); - } + protected override T DefaultMinValue => T.MinValue; - 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; - } - } + protected override T DefaultMaxValue => T.MaxValue; /// /// The default . @@ -180,26 +98,12 @@ 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; + if (typeof(T) == typeof(double)) + return (T)(object)double.Epsilon; - return (T)(object)double.Epsilon; + return T.One; } } @@ -249,63 +153,11 @@ public override void UnbindEvents() 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 Set(TNewValue val) where TNewValue : struct, INumber + => Value = T.CreateTruncating(val); - 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); - } + public void Add(TNewValue val) where TNewValue : struct, INumber + => Value += T.CreateTruncating(val); /// /// Sets the value of the bindable to Min + (Max - Min) * amt @@ -314,8 +166,10 @@ public void Add(TNewValue val) where TNewValue : struct, /// public void SetProportional(float amt, float snap = 0) { - double min = MinValue.ToDouble(NumberFormatInfo.InvariantInfo); - double max = MaxValue.ToDouble(NumberFormatInfo.InvariantInfo); + // TODO: Use IFloatingPointIeee754.Lerp when applicable + + double min = double.CreateTruncating(MinValue); + double max = double.CreateTruncating(MaxValue); double value = min + (max - min) * amt; if (snap > 0) value = Math.Round(value / snap) * snap; @@ -350,20 +204,8 @@ public override bool 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; + protected sealed override T ClampValue(T value, T minValue, T maxValue) => T.Clamp(value, minValue, maxValue); - private static T max(T value1, T value2) - { - int comparison = value1.CompareTo(value2); - return comparison > 0 ? value1 : value2; - } - - private static T min(T value1, T value2) - { - int comparison = value1.CompareTo(value2); - return comparison > 0 ? value2 : value1; - } + protected sealed override bool IsValidRange(T min, T max) => min <= max; } } diff --git a/osu.Framework/Bindables/BindableNumberWithCurrent.cs b/osu.Framework/Bindables/BindableNumberWithCurrent.cs index 60c403f19f..6064d65691 100644 --- a/osu.Framework/Bindables/BindableNumberWithCurrent.cs +++ b/osu.Framework/Bindables/BindableNumberWithCurrent.cs @@ -4,6 +4,7 @@ #nullable disable using System; +using System.Numerics; namespace osu.Framework.Bindables { @@ -12,7 +13,7 @@ namespace osu.Framework.Bindables /// /// The type of our stored . public class BindableNumberWithCurrent : BindableNumber, IBindableWithCurrent - where T : struct, IComparable, IConvertible, IEquatable + where T : struct, INumber, IMinMaxValue { private BindableNumber currentBound; diff --git a/osu.Framework/Bindables/RangeConstrainedBindable.cs b/osu.Framework/Bindables/RangeConstrainedBindable.cs index efc3dda987..8721ecc413 100644 --- a/osu.Framework/Bindables/RangeConstrainedBindable.cs +++ b/osu.Framework/Bindables/RangeConstrainedBindable.cs @@ -205,8 +205,10 @@ public override void CopyTo(Bindable them) // as Value assignment (in the base call below) automatically clamps to [MinValue, MaxValue]. if (them is RangeConstrainedBindable other) { - other.MinValue = MinValue; - other.MaxValue = MaxValue; + // copy the bounds over without updating the current value, to avoid clamping on invalid ranges. + // there is no need to clamp `Value` after that directly - the `base.CopyTo()` call will change `Value` anyway. + other.SetMinValue(MinValue, false, this); + other.SetMaxValue(MaxValue, false, this); } base.CopyTo(them); diff --git a/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs b/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs index 2babb3aa15..17b932cee9 100644 --- a/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs +++ b/osu.Framework/Graphics/UserInterface/BasicSliderBar.cs @@ -1,15 +1,15 @@ // 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 osuTK; +using System.Numerics; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; +using Vector2 = osuTK.Vector2; namespace osu.Framework.Graphics.UserInterface { public partial class BasicSliderBar : SliderBar - where T : struct, IComparable, IConvertible, IEquatable + where T : struct, INumber, IMinMaxValue { public Color4 BackgroundColour { diff --git a/osu.Framework/Graphics/UserInterface/SliderBar.cs b/osu.Framework/Graphics/UserInterface/SliderBar.cs index 6ff8307204..9b173f832e 100644 --- a/osu.Framework/Graphics/UserInterface/SliderBar.cs +++ b/osu.Framework/Graphics/UserInterface/SliderBar.cs @@ -2,17 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Globalization; +using System.Numerics; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; -using osuTK.Input; -using osuTK; using osu.Framework.Input.Events; +using osuTK.Input; +using Vector2 = osuTK.Vector2; namespace osu.Framework.Graphics.UserInterface { public abstract partial class SliderBar : Container, IHasCurrentValue - where T : struct, IComparable, IConvertible, IEquatable + where T : struct, INumber, IMinMaxValue { /// /// Range padding reduces the range of movement a slider bar is allowed to have @@ -121,9 +121,9 @@ protected override bool OnMouseDown(MouseDownEvent e) { if (ShouldHandleAsRelativeDrag(e)) { - float min = currentNumberInstantaneous.MinValue.ToSingle(NumberFormatInfo.InvariantInfo); - float max = currentNumberInstantaneous.MaxValue.ToSingle(NumberFormatInfo.InvariantInfo); - float val = currentNumberInstantaneous.Value.ToSingle(NumberFormatInfo.InvariantInfo); + float min = float.CreateTruncating(currentNumberInstantaneous.MinValue); + float max = float.CreateTruncating(currentNumberInstantaneous.MaxValue); + float val = float.CreateTruncating(currentNumberInstantaneous.Value); relativeValueAtMouseDown = (val - min) / (max - min); diff --git a/osu.Framework/Testing/Drawables/Steps/StepSlider.cs b/osu.Framework/Testing/Drawables/Steps/StepSlider.cs index dbf497a1c3..c8931db818 100644 --- a/osu.Framework/Testing/Drawables/Steps/StepSlider.cs +++ b/osu.Framework/Testing/Drawables/Steps/StepSlider.cs @@ -13,11 +13,12 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Input.Events; using osuTK.Input; +using System.Numerics; namespace osu.Framework.Testing.Drawables.Steps { public partial class StepSlider : SliderBar - where T : struct, IComparable, IConvertible, IEquatable + where T : struct, INumber, IMinMaxValue { private readonly Box selection; private readonly Box background; diff --git a/osu.Framework/Testing/TestScene.cs b/osu.Framework/Testing/TestScene.cs index df58bbddf9..c0dca8ec2f 100644 --- a/osu.Framework/Testing/TestScene.cs +++ b/osu.Framework/Testing/TestScene.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Numerics; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -26,9 +27,9 @@ using osu.Framework.Platform; using osu.Framework.Testing.Drawables.Steps; using osu.Framework.Threading; -using osuTK; using osuTK.Graphics; using Logger = osu.Framework.Logging.Logger; +using Vector2 = osuTK.Vector2; namespace osu.Framework.Testing { @@ -366,7 +367,7 @@ protected void AddWaitStep(string description, int waitCount) => schedule(() => }); }); - protected void AddSliderStep(string description, T min, T max, T start, Action valueChanged) where T : struct, IComparable, IConvertible, IEquatable => schedule(() => + protected void AddSliderStep(string description, T min, T max, T start, Action valueChanged) where T : struct, INumber, IMinMaxValue => schedule(() => { StepsContainer.Add(new StepSlider(description, min, max, start) {