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.Globalization;
6using osu.Framework.Bindables;
7using osu.Framework.Graphics;
8using osu.Framework.Graphics.Containers;
9using osu.Framework.Graphics.Sprites;
10
11namespace osu.Framework.Tests.Visual.Bindables
12{
13 public class TestSceneBindableNumbers : FrameworkTestScene
14 {
15 private readonly BindableInt bindableInt = new BindableInt();
16 private readonly BindableLong bindableLong = new BindableLong();
17 private readonly BindableDouble bindableDouble = new BindableDouble();
18 private readonly BindableFloat bindableFloat = new BindableFloat();
19
20 public TestSceneBindableNumbers()
21 {
22 AddStep("Reset", () =>
23 {
24 setValue(0);
25 setPrecision(1);
26 });
27
28 testBasic();
29 testPrecision3();
30 testPrecision10();
31 testMinMaxWithoutPrecision();
32 testMinMaxWithPrecision();
33 testInvalidPrecision();
34 testFractionalPrecision();
35
36 AddSliderStep("Min value", -100, 100, -100, setMin);
37 AddSliderStep("Max value", -100, 100, 100, setMax);
38 AddSliderStep("Value", -100, 100, 0, setValue);
39 AddSliderStep("Precision", 1, 10, 1, setPrecision);
40
41 Child = new GridContainer
42 {
43 RelativeSizeAxes = Axes.Both,
44 Content = new[]
45 {
46 new Drawable[]
47 {
48 new BindableDisplayContainer<int>(bindableInt),
49 new BindableDisplayContainer<long>(bindableLong),
50 },
51 new Drawable[]
52 {
53 new BindableDisplayContainer<float>(bindableFloat),
54 new BindableDisplayContainer<double>(bindableDouble),
55 }
56 }
57 };
58 }
59
60 /// <summary>
61 /// Tests basic setting of values.
62 /// </summary>
63 private void testBasic()
64 {
65 AddStep("Value = 10", () => setValue(10));
66 AddAssert("Check = 10", () => checkExact(10));
67 }
68
69 /// <summary>
70 /// Tests that midpoint values are correctly rounded with a precision of 3.
71 /// </summary>
72 private void testPrecision3()
73 {
74 AddStep("Precision = 3", () => setPrecision(3));
75 AddStep("Value = 4", () => setValue(3));
76 AddAssert("Check = 3", () => checkExact(3));
77 AddStep("Value = 5", () => setValue(5));
78 AddAssert("Check = 6", () => checkExact(6));
79 AddStep("Value = 59", () => setValue(59));
80 AddAssert("Check = 60", () => checkExact(60));
81 }
82
83 /// <summary>
84 /// Tests that midpoint values are correctly rounded with a precision of 10.
85 /// </summary>
86 private void testPrecision10()
87 {
88 AddStep("Precision = 10", () => setPrecision(10));
89 AddStep("Value = 6", () => setValue(6));
90 AddAssert("Check = 10", () => checkExact(10));
91 }
92
93 /// <summary>
94 /// Tests that values are correctly clamped to min/max values.
95 /// </summary>
96 private void testMinMaxWithoutPrecision()
97 {
98 AddStep("Precision = 1", () => setPrecision(1));
99 AddStep("Min = -30", () => setMin(-30));
100 AddStep("Max = 30", () => setMax(30));
101 AddStep("Value = -50", () => setValue(-50));
102 AddAssert("Check = -30", () => checkExact(-30));
103 AddStep("Value = 50", () => setValue(50));
104 AddAssert("Check = 30", () => checkExact(30));
105 }
106
107 /// <summary>
108 /// Tests that values are correctly clamped to min/max values when precision is involved.
109 /// In this case, precision is preferred over min/max values.
110 /// </summary>
111 private void testMinMaxWithPrecision()
112 {
113 AddStep("Precision = 5", () => setPrecision(5));
114 AddStep("Min = -27", () => setMin(-27));
115 AddStep("Max = 27", () => setMax(27));
116 AddStep("Value = -30", () => setValue(-30));
117 AddAssert("Check = -25", () => checkExact(-25));
118 AddStep("Value = 30", () => setValue(30));
119 AddAssert("Check = 25", () => checkExact(25));
120 }
121
122 /// <summary>
123 /// Tests that invalid precisions cause exceptions.
124 /// </summary>
125 private void testInvalidPrecision()
126 {
127 AddAssert("Precision = 0 throws", () =>
128 {
129 try
130 {
131 setPrecision(0);
132 return false;
133 }
134 catch (Exception)
135 {
136 return true;
137 }
138 });
139
140 AddAssert("Precision = -1 throws", () =>
141 {
142 try
143 {
144 setPrecision(-1);
145 return false;
146 }
147 catch (Exception)
148 {
149 return true;
150 }
151 });
152 }
153
154 /// <summary>
155 /// Tests that fractional precisions are obeyed.
156 /// Note that int bindables are assigned int precisions/values, so their results will differ.
157 /// </summary>
158 private void testFractionalPrecision()
159 {
160 AddStep("Precision = 2.25/2", () => setPrecision(2.25));
161 AddStep("Value = 3.3/3", () => setValue(3.3));
162 AddAssert("Check = 2.25/4", () => checkExact(2.25m, 4));
163 AddStep("Value = 4.17/4", () => setValue(4.17));
164 AddAssert("Check = 4.5/4", () => checkExact(4.5m, 4));
165 }
166
167 private bool checkExact(decimal value) => checkExact(value, value);
168
169 private bool checkExact(decimal floatValue, decimal intValue)
170 => bindableInt.Value == Convert.ToInt32(intValue)
171 && bindableLong.Value == Convert.ToInt64(intValue)
172 && bindableFloat.Value == Convert.ToSingle(floatValue)
173 && bindableDouble.Value == Convert.ToDouble(floatValue);
174
175 private void setMin<T>(T value)
176 {
177 bindableInt.MinValue = Convert.ToInt32(value);
178 bindableLong.MinValue = Convert.ToInt64(value);
179 bindableFloat.MinValue = Convert.ToSingle(value);
180 bindableDouble.MinValue = Convert.ToDouble(value);
181 }
182
183 private void setMax<T>(T value)
184 {
185 bindableInt.MaxValue = Convert.ToInt32(value);
186 bindableLong.MaxValue = Convert.ToInt64(value);
187 bindableFloat.MaxValue = Convert.ToSingle(value);
188 bindableDouble.MaxValue = Convert.ToDouble(value);
189 }
190
191 private void setValue<T>(T value)
192 {
193 bindableInt.Value = Convert.ToInt32(value);
194 bindableLong.Value = Convert.ToInt64(value);
195 bindableFloat.Value = Convert.ToSingle(value);
196 bindableDouble.Value = Convert.ToDouble(value);
197 }
198
199 private void setPrecision<T>(T precision)
200 {
201 bindableInt.Precision = Convert.ToInt32(precision);
202 bindableLong.Precision = Convert.ToInt64(precision);
203 bindableFloat.Precision = Convert.ToSingle(precision);
204 bindableDouble.Precision = Convert.ToDouble(precision);
205 }
206
207 private class BindableDisplayContainer<T> : CompositeDrawable
208 where T : struct, IComparable<T>, IConvertible, IEquatable<T>
209 {
210 public BindableDisplayContainer(BindableNumber<T> bindable)
211 {
212 Anchor = Anchor.Centre;
213 Origin = Anchor.Centre;
214
215 SpriteText valueText;
216 InternalChild = new FillFlowContainer
217 {
218 AutoSizeAxes = Axes.Both,
219 Direction = FillDirection.Vertical,
220 Children = new Drawable[]
221 {
222 new SpriteText { Text = $"{typeof(T).Name} value:" },
223 valueText = new SpriteText { Text = bindable.Value.ToString(CultureInfo.InvariantCulture) }
224 }
225 };
226
227 bindable.ValueChanged += e => valueText.Text = e.NewValue.ToString(CultureInfo.InvariantCulture);
228 }
229 }
230 }
231}