A game framework written with osu! in mind.
at master 237 lines 8.2 kB view raw
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}