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 osu.Framework.Bindables;
6using osu.Framework.Graphics.Containers;
7using osuTK.Input;
8using osuTK;
9using osu.Framework.Input.Events;
10
11namespace osu.Framework.Graphics.UserInterface
12{
13 public abstract class SliderBar<T> : Container, IHasCurrentValue<T>
14 where T : struct, IComparable<T>, IConvertible, IEquatable<T>
15 {
16 /// <summary>
17 /// Range padding reduces the range of movement a slider bar is allowed to have
18 /// while still receiving input in the padded region. This behavior is necessary
19 /// for finite-sized nubs and can not be achieved (currently) by existing
20 /// scene graph padding / margin functionality.
21 /// </summary>
22 public float RangePadding;
23
24 /// <summary>
25 /// Whether keyboard control should be allowed even when the bar is not hovered.
26 /// </summary>
27 [Obsolete("Implement this kind of behaviour separately instead.")] // Can be removed 20220107
28 protected virtual bool AllowKeyboardInputWhenNotHovered => false;
29
30 public float UsableWidth => DrawWidth - 2 * RangePadding;
31
32 /// <summary>
33 /// A custom step value for each key press which actuates a change on this control.
34 /// </summary>
35 public float KeyboardStep;
36
37 private readonly BindableNumber<T> currentNumberInstantaneous;
38
39 /// <summary>
40 /// When set, value changes based on user input are only transferred to any bound <see cref="Current"/> on commit.
41 /// This is useful if the UI interaction could be adversely affected by the value changing, such as the position of the <see cref="SliderBar{T}"/> on the screen.
42 /// </summary>
43 public bool TransferValueOnCommit;
44
45 private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<T>();
46
47 protected BindableNumber<T> CurrentNumber => current;
48
49 public Bindable<T> Current
50 {
51 get => current;
52 set
53 {
54 if (value == null)
55 throw new ArgumentNullException(nameof(value));
56
57 current.Current = value;
58
59 currentNumberInstantaneous.Default = current.Default;
60 }
61 }
62
63 protected SliderBar()
64 {
65 currentNumberInstantaneous = new BindableNumber<T>();
66
67 current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue;
68 current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v;
69 current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v;
70 current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v;
71 current.DisabledChanged += v => currentNumberInstantaneous.Disabled = v;
72
73 currentNumberInstantaneous.ValueChanged += e =>
74 {
75 if (!TransferValueOnCommit)
76 current.Value = e.NewValue;
77 };
78 }
79
80 protected float NormalizedValue
81 {
82 get
83 {
84 if (!currentNumberInstantaneous.HasDefinedRange)
85 {
86 throw new InvalidOperationException($"A {nameof(SliderBar<T>)}'s {nameof(Current)} must have user-defined {nameof(BindableNumber<T>.MinValue)}"
87 + $" and {nameof(BindableNumber<T>.MaxValue)} to produce a valid {nameof(NormalizedValue)}.");
88 }
89
90 var min = Convert.ToSingle(currentNumberInstantaneous.MinValue);
91 var max = Convert.ToSingle(currentNumberInstantaneous.MaxValue);
92
93 if (max - min == 0)
94 return 1;
95
96 var val = Convert.ToSingle(currentNumberInstantaneous.Value);
97 return (val - min) / (max - min);
98 }
99 }
100
101 /// <summary>
102 /// Triggered when the <see cref="Current"/> value has changed. Used to update the displayed value.
103 /// </summary>
104 /// <param name="value">The normalized <see cref="Current"/> value.</param>
105 protected abstract void UpdateValue(float value);
106
107 protected override void LoadComplete()
108 {
109 base.LoadComplete();
110
111 currentNumberInstantaneous.ValueChanged += _ => Scheduler.AddOnce(updateValue);
112 currentNumberInstantaneous.MinValueChanged += _ => Scheduler.AddOnce(updateValue);
113 currentNumberInstantaneous.MaxValueChanged += _ => Scheduler.AddOnce(updateValue);
114
115 Scheduler.AddOnce(updateValue);
116 }
117
118 private void updateValue() => UpdateValue(NormalizedValue);
119
120 private bool handleClick;
121
122 protected override bool OnMouseDown(MouseDownEvent e)
123 {
124 handleClick = true;
125 return base.OnMouseDown(e);
126 }
127
128 protected override bool OnClick(ClickEvent e)
129 {
130 if (handleClick)
131 {
132 handleMouseInput(e);
133 commit();
134 }
135
136 return true;
137 }
138
139 protected override void OnDrag(DragEvent e)
140 {
141 handleMouseInput(e);
142 }
143
144 protected override bool OnDragStart(DragStartEvent e)
145 {
146 Vector2 posDiff = e.MouseDownPosition - e.MousePosition;
147
148 if (Math.Abs(posDiff.X) < Math.Abs(posDiff.Y))
149 {
150 handleClick = false;
151 return false;
152 }
153
154 handleMouseInput(e);
155 return true;
156 }
157
158 protected override void OnDragEnd(DragEndEvent e)
159 {
160 handleMouseInput(e);
161 commit();
162 }
163
164 protected override bool OnKeyDown(KeyDownEvent e)
165 {
166 if (currentNumberInstantaneous.Disabled)
167 return false;
168
169#pragma warning disable 618
170 bool shouldHandle = IsHovered || AllowKeyboardInputWhenNotHovered;
171#pragma warning restore 618
172 if (!shouldHandle)
173 return false;
174
175 var step = KeyboardStep != 0 ? KeyboardStep : (Convert.ToSingle(currentNumberInstantaneous.MaxValue) - Convert.ToSingle(currentNumberInstantaneous.MinValue)) / 20;
176 if (currentNumberInstantaneous.IsInteger) step = MathF.Ceiling(step);
177
178 switch (e.Key)
179 {
180 case Key.Right:
181 currentNumberInstantaneous.Add(step);
182 onUserChange(currentNumberInstantaneous.Value);
183 return true;
184
185 case Key.Left:
186 currentNumberInstantaneous.Add(-step);
187 onUserChange(currentNumberInstantaneous.Value);
188 return true;
189
190 default:
191 return false;
192 }
193 }
194
195 protected override void OnKeyUp(KeyUpEvent e)
196 {
197 if (e.Key == Key.Left || e.Key == Key.Right)
198 commit();
199 }
200
201 private bool uncommittedChanges;
202
203 private bool commit()
204 {
205 if (!uncommittedChanges)
206 return false;
207
208 current.Value = currentNumberInstantaneous.Value;
209 uncommittedChanges = false;
210 return true;
211 }
212
213 private void handleMouseInput(UIEvent e)
214 {
215 var xPosition = ToLocalSpace(e.ScreenSpaceMousePosition).X - RangePadding;
216
217 if (currentNumberInstantaneous.Disabled)
218 return;
219
220 currentNumberInstantaneous.SetProportional(xPosition / UsableWidth, e.ShiftPressed ? KeyboardStep : 0);
221 onUserChange(currentNumberInstantaneous.Value);
222 }
223
224 private void onUserChange(T value)
225 {
226 uncommittedChanges = true;
227 OnUserChange(value);
228 }
229
230 /// <summary>
231 /// Triggered when the value is changed based on end-user input to this control.
232 /// </summary>
233 protected virtual void OnUserChange(T value)
234 {
235 }
236 }
237}