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.Collections.Generic;
6
7namespace osu.Framework.Bindables
8{
9 public abstract class RangeConstrainedBindable<T> : Bindable<T>
10 {
11 public event Action<T> MinValueChanged;
12
13 public event Action<T> MaxValueChanged;
14
15 private T minValue;
16
17 public T MinValue
18 {
19 get => minValue;
20 set
21 {
22 if (EqualityComparer<T>.Default.Equals(value, minValue))
23 return;
24
25 SetMinValue(value, true, this);
26 }
27 }
28
29 private T maxValue;
30
31 public T MaxValue
32 {
33 get => maxValue;
34 set
35 {
36 if (EqualityComparer<T>.Default.Equals(value, maxValue))
37 return;
38
39 SetMaxValue(value, true, this);
40 }
41 }
42
43 public override T Value
44 {
45 get => base.Value;
46 set => setValue(value);
47 }
48
49 /// <summary>
50 /// The default <see cref="MinValue"/>. This should be equal to the minimum value of type <typeparamref name="T"/>.
51 /// </summary>
52 protected abstract T DefaultMinValue { get; }
53
54 /// <summary>
55 /// The default <see cref="MaxValue"/>. This should be equal to the maximum value of type <typeparamref name="T"/>.
56 /// </summary>
57 protected abstract T DefaultMaxValue { get; }
58
59 /// <summary>
60 /// Whether this bindable has a user-defined range that is not the full range of the <typeparamref name="T"/> type.
61 /// </summary>
62 public bool HasDefinedRange => !EqualityComparer<T>.Default.Equals(MinValue, DefaultMinValue) ||
63 !EqualityComparer<T>.Default.Equals(MaxValue, DefaultMaxValue);
64
65 protected RangeConstrainedBindable(T defaultValue = default)
66 : base(defaultValue)
67 {
68 minValue = DefaultMinValue;
69 maxValue = DefaultMaxValue;
70
71 // Reapply the default value here for respecting the defined default min/max values.
72 setValue(defaultValue);
73 }
74
75 /// <summary>
76 /// Sets the minimum value. This method does no equality comparisons.
77 /// </summary>
78 /// <param name="minValue">The new minimum value.</param>
79 /// <param name="updateCurrentValue">Whether to update the current value after the minimum value is set.</param>
80 /// <param name="source">The bindable that triggered this. A null value represents the current bindable instance.</param>
81 internal void SetMinValue(T minValue, bool updateCurrentValue, RangeConstrainedBindable<T> source)
82 {
83 this.minValue = minValue;
84 TriggerMinValueChange(source);
85
86 if (updateCurrentValue)
87 {
88 // Reapply the current value to respect the new minimum value.
89 setValue(Value);
90 }
91 }
92
93 /// <summary>
94 /// Sets the maximum value. This method does no equality comparisons.
95 /// </summary>
96 /// <param name="maxValue">The new maximum value.</param>
97 /// <param name="updateCurrentValue">Whether to update the current value after the maximum value is set.</param>
98 /// <param name="source">The bindable that triggered this. A null value represents the current bindable instance.</param>
99 internal void SetMaxValue(T maxValue, bool updateCurrentValue, RangeConstrainedBindable<T> source)
100 {
101 this.maxValue = maxValue;
102 TriggerMaxValueChange(source);
103
104 if (updateCurrentValue)
105 {
106 // Reapply the current value to respect the new maximum value.
107 setValue(Value);
108 }
109 }
110
111 public override void TriggerChange()
112 {
113 base.TriggerChange();
114
115 TriggerMinValueChange(this, false);
116 TriggerMaxValueChange(this, false);
117 }
118
119 protected void TriggerMinValueChange(RangeConstrainedBindable<T> source = null, bool propagateToBindings = true)
120 {
121 // check a bound bindable hasn't changed the value again (it will fire its own event)
122 T beforePropagation = minValue;
123
124 if (propagateToBindings && Bindings != null)
125 {
126 foreach (var b in Bindings)
127 {
128 if (b == source) continue;
129
130 if (b is RangeConstrainedBindable<T> cb)
131 cb.SetMinValue(minValue, false, this);
132 }
133 }
134
135 if (EqualityComparer<T>.Default.Equals(beforePropagation, minValue))
136 MinValueChanged?.Invoke(minValue);
137 }
138
139 protected void TriggerMaxValueChange(RangeConstrainedBindable<T> source = null, bool propagateToBindings = true)
140 {
141 // check a bound bindable hasn't changed the value again (it will fire its own event)
142 T beforePropagation = maxValue;
143
144 if (propagateToBindings && Bindings != null)
145 {
146 foreach (var b in Bindings)
147 {
148 if (b == source) continue;
149
150 if (b is RangeConstrainedBindable<T> cb)
151 cb.SetMaxValue(maxValue, false, this);
152 }
153 }
154
155 if (EqualityComparer<T>.Default.Equals(beforePropagation, maxValue))
156 MaxValueChanged?.Invoke(maxValue);
157 }
158
159 public override void BindTo(Bindable<T> them)
160 {
161 if (them is RangeConstrainedBindable<T> other)
162 {
163 if (!IsValidRange(other.MinValue, other.MaxValue))
164 {
165 throw new ArgumentOutOfRangeException(
166 nameof(them), $"The target bindable has specified an invalid range of [{other.MinValue} - {other.MaxValue}].");
167 }
168
169 MinValue = other.MinValue;
170 MaxValue = other.MaxValue;
171 }
172
173 base.BindTo(them);
174 }
175
176 public override void UnbindEvents()
177 {
178 base.UnbindEvents();
179
180 MinValueChanged = null;
181 MaxValueChanged = null;
182 }
183
184 public new RangeConstrainedBindable<T> GetBoundCopy() => (RangeConstrainedBindable<T>)base.GetBoundCopy();
185
186 public new RangeConstrainedBindable<T> GetUnboundCopy() => (RangeConstrainedBindable<T>)base.GetUnboundCopy();
187
188 /// <summary>
189 /// Clamps <paramref name="value"/> to the range defined by <paramref name="minValue"/> and <paramref name="maxValue"/>.
190 /// </summary>
191 protected abstract T ClampValue(T value, T minValue, T maxValue);
192
193 /// <summary>
194 /// Whether <paramref name="min"/> and <paramref name="max"/> constitute a valid range
195 /// (usually used to check that <paramref name="min"/> is indeed lesser than or equal to <paramref name="max"/>).
196 /// </summary>
197 /// <param name="min">The range's minimum value.</param>
198 /// <param name="max">The range's maximum value.</param>
199 protected abstract bool IsValidRange(T min, T max);
200
201 private void setValue(T value) => base.Value = ClampValue(value, minValue, maxValue);
202 }
203}