Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom mouse step and range properties to SliderBar #6376

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneSliderBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable disable

using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
Expand Down Expand Up @@ -113,6 +114,9 @@ public TestSceneSliderBar()
{
sliderBar.Current.Disabled = false;
sliderBar.Current.Value = 0;
sliderBar.MouseStep = 0;
sliderBar.MinValue = sliderBarValue.MinValue;
sliderBar.MaxValue = sliderBarValue.MaxValue;
sliderBar.GetContainingFocusManager()!.ChangeFocus(null);
sliderBarWithNub.GetContainingFocusManager()!.ChangeFocus(null);
});
Expand Down Expand Up @@ -187,19 +191,23 @@ public void TestKeyboardInput()
checkValue(-4);
}

[TestCase(false)]
[TestCase(true)]
public void TestAdjustmentPrecision(bool disabled)
[TestCase(false, 0)]
[TestCase(false, 1)]
[TestCase(false, 3)]
[TestCase(true, 0)]
public void TestAdjustmentPrecision(bool disabled, float step)
{
AddStep($"set disabled to {disabled}", () => sliderBar.Current.Disabled = disabled);

AddStep($"Set step to {step}", () => sliderBar.MouseStep = step);

AddStep("Click at 25% mark", () =>
{
InputManager.MoveMouseTo(sliderBar.ToScreenSpace(sliderBar.DrawSize * new Vector2(0.25f, 0.5f)));
InputManager.Click(MouseButton.Left);
});
// We're translating to/from screen-space coordinates for click coordinates so we want to be more lenient with the value comparisons in these tests
checkValue(disabled ? 0 : -5);
checkValue(disabled ? 0 : step == 0 ? -5 : MathF.Round(-5 / step) * step);
AddStep("Press left arrow key", () =>
{
bool before = sliderBar.IsHovered;
Expand All @@ -208,7 +216,7 @@ public void TestAdjustmentPrecision(bool disabled)
InputManager.ReleaseKey(Key.Left);
sliderBar.IsHovered = before;
});
checkValue(disabled ? 0 : -6);
checkValue(disabled ? 0 : step == 0 ? -6 : MathF.Round(-5 / step) * step - 1);
AddStep("Click at 75% mark, holding shift", () =>
{
InputManager.PressKey(Key.LShift);
Expand All @@ -219,6 +227,56 @@ public void TestAdjustmentPrecision(bool disabled)
checkValue(disabled ? 0 : 5);
}

[Test]
public void TestCustomRange()
{
AddStep("Set range to -5 to 5", () =>
{
sliderBar.MinValue = -5;
sliderBar.MaxValue = 5;
});

AddStep("Click at 25% mark", () =>
{
InputManager.MoveMouseTo(sliderBar.ToScreenSpace(sliderBar.DrawSize * new Vector2(0.25f, 0.5f)));
InputManager.Click(MouseButton.Left);
});
checkValue(-2.5f);

AddStep("Press right arrow key", () =>
{
bool before = sliderBar.IsHovered;
sliderBar.IsHovered = true;
InputManager.PressKey(Key.Right);
InputManager.ReleaseKey(Key.Right);
sliderBar.IsHovered = before;
});
checkValue(-1.5f);

AddStep("Set value to 10", () => sliderBarValue.Value = 10);
AddStep("Press left arrow key", () =>
{
bool before = sliderBar.IsHovered;
sliderBar.IsHovered = true;
InputManager.PressKey(Key.Left);
InputManager.ReleaseKey(Key.Left);
sliderBar.IsHovered = before;
});
checkValue(4f);

AddStep("Press right arrow key twice", () =>
{
bool before = sliderBar.IsHovered;
sliderBar.IsHovered = true;
InputManager.PressKey(Key.Right);
InputManager.ReleaseKey(Key.Right);
InputManager.PressKey(Key.Right);
InputManager.ReleaseKey(Key.Right);
sliderBar.IsHovered = before;
});
checkValue(5f);
}

[TestCase(false)]
[TestCase(true)]
public void TestTransferValueOnCommit(bool disabled)
Expand Down Expand Up @@ -299,7 +357,7 @@ public void TestRelativeClick()
checkValue(0);
}

private void checkValue(int expected) =>
private void checkValue(float expected) =>
AddAssert($"Value == {expected}", () => sliderBarValue.Value, () => Is.EqualTo(expected).Within(Precision.FLOAT_EPSILON));

private void sliderBarValueChanged(ValueChangedEvent<double> args)
Expand Down
124 changes: 110 additions & 14 deletions osu.Framework/Graphics/UserInterface/SliderBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Numerics;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
Expand All @@ -24,10 +25,77 @@ public abstract partial class SliderBar<T> : Container, IHasCurrentValue<T>

public float UsableWidth => DrawWidth - 2 * RangePadding;

private T mouseStep;

/// <summary>
/// A custom step value for mouse input which actuates a change on this control.
/// </summary>
public T MouseStep
{
get => mouseStep;
set
{
T multiple = value / currentNumberInstantaneous.Precision;
if (!T.IsNaN(multiple) && !T.IsInfinity(multiple) && !T.IsZero(multiple % T.One))
throw new ArgumentException("Mouse step must be a multiple of the bindable precision.");

mouseStep = value;
}
}

private T keyboardStep;

/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep;
public T KeyboardStep
{
get => keyboardStep;
set
{
T multiple = value / currentNumberInstantaneous.Precision;
if (!T.IsNaN(multiple) && !T.IsInfinity(multiple) && !T.IsZero(multiple % T.One))
throw new ArgumentException("Keyboard step must be a multiple of the bindable precision.");

keyboardStep = value;
}
}

private T minValue = T.MinValue;

public T MinValue
{
get => T.Max(minValue, currentNumberInstantaneous.MinValue);
set
{
if (value < currentNumberInstantaneous.MinValue)
throw new ArgumentException("Minimum value override must be greater than or equal to the bindable minimum value.");

if (EqualityComparer<T>.Default.Equals(value, minValue))
return;

minValue = value;
Scheduler.AddOnce(updateValue);
}
}

private T maxValue = T.MaxValue;

public T MaxValue
{
get => T.Min(maxValue, currentNumberInstantaneous.MaxValue);
set
{
if (value > currentNumberInstantaneous.MaxValue)
throw new ArgumentException("Maximum value override must be less than or equal to the bindable maximum value.");

if (EqualityComparer<T>.Default.Equals(value, maxValue))
return;

maxValue = value;
Scheduler.AddOnce(updateValue);
}
}

private readonly BindableNumber<T> currentNumberInstantaneous;

Expand Down Expand Up @@ -81,17 +149,28 @@ protected SliderBar()
};
}

protected bool HasDefinedRange => !EqualityComparer<T>.Default.Equals(MinValue, T.MinValue) ||
!EqualityComparer<T>.Default.Equals(MaxValue, T.MaxValue);

protected float NormalizedValue
{
get
{
if (!currentNumberInstantaneous.HasDefinedRange)
if (!HasDefinedRange)
{
throw new InvalidOperationException($"A {nameof(SliderBar<T>)}'s {nameof(Current)} must have user-defined {nameof(BindableNumber<T>.MinValue)}"
throw new InvalidOperationException($"A {nameof(SliderBar<T>)} must have user-defined {nameof(MinValue)} and {nameof(MaxValue)}"
+ $" or {nameof(Current)} must have user-defined {nameof(BindableNumber<T>.MinValue)}"
+ $" and {nameof(BindableNumber<T>.MaxValue)} to produce a valid {nameof(NormalizedValue)}.");
}

return currentNumberInstantaneous.NormalizedValue;
float min = float.CreateTruncating(MinValue);
float max = float.CreateTruncating(MaxValue);

if (max - min == 0)
return 1;

float val = float.CreateTruncating(currentNumberInstantaneous.Value);
return (val - min) / (max - min);
}
}

Expand Down Expand Up @@ -121,11 +200,7 @@ protected override bool OnMouseDown(MouseDownEvent e)
{
if (ShouldHandleAsRelativeDrag(e))
{
float min = float.CreateTruncating(currentNumberInstantaneous.MinValue);
float max = float.CreateTruncating(currentNumberInstantaneous.MaxValue);
float val = float.CreateTruncating(currentNumberInstantaneous.Value);

relativeValueAtMouseDown = (val - min) / (max - min);
relativeValueAtMouseDown = NormalizedValue;

// Click shouldn't be handled if relative dragging is happening (i.e. while holding a nub).
// This is generally an expectation by most OSes and UIs.
Expand Down Expand Up @@ -183,18 +258,20 @@ protected override bool OnKeyDown(KeyDownEvent e)
if (!IsHovered && !HasFocus)
return false;

float step = KeyboardStep != 0 ? KeyboardStep : (Convert.ToSingle(currentNumberInstantaneous.MaxValue) - Convert.ToSingle(currentNumberInstantaneous.MinValue)) / 20;
if (currentNumberInstantaneous.IsInteger) step = MathF.Ceiling(step);
T step = !T.IsZero(KeyboardStep) ? KeyboardStep : (MaxValue - MinValue) / T.CreateTruncating(20);
if (currentNumberInstantaneous.IsInteger) step = T.Max(step, T.One);

T clampedCurrent = clamp(currentNumberInstantaneous.Value);

switch (e.Key)
{
case Key.Right:
currentNumberInstantaneous.Add(step);
currentNumberInstantaneous.Set(clamp(clampedCurrent + step));
onUserChange(currentNumberInstantaneous.Value);
return true;

case Key.Left:
currentNumberInstantaneous.Add(-step);
currentNumberInstantaneous.Set(clamp(clampedCurrent - step));
onUserChange(currentNumberInstantaneous.Value);
return true;

Expand All @@ -203,6 +280,8 @@ protected override bool OnKeyDown(KeyDownEvent e)
}
}

private T clamp(T value) => T.Clamp(value, MinValue, MaxValue);

protected override void OnKeyUp(KeyUpEvent e)
{
if (e.Key == Key.Left || e.Key == Key.Right)
Expand Down Expand Up @@ -250,10 +329,27 @@ private void handleMouseInput(MouseButtonEvent e)
newValue = (localX - RangePadding) / UsableWidth;
}

currentNumberInstantaneous.SetProportional(newValue, e.ShiftPressed ? KeyboardStep : 0);
float snap = e.ShiftPressed ? float.CreateTruncating(KeyboardStep) : float.CreateTruncating(MouseStep);
setProportional(newValue, snap);
onUserChange(currentNumberInstantaneous.Value);
}

/// <summary>
/// Sets the value of <see cref="currentNumberInstantaneous"/> to <see cref="MinValue"/> + (<see cref="MaxValue"/> - <see cref="MinValue"/>) * amt
/// <param name="amt">The proportional amount to set, ranging from 0 to 1.</param>
/// <param name="snap">If greater than 0, snap the final value to the closest multiple of this number.</param>
/// </summary>
private void setProportional(float amt, float snap = 0)
{
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;
value = Math.Clamp(value, min, max);
currentNumberInstantaneous.Set(value);
}

private void onUserChange(T value)
{
uncommittedChanges = true;
Expand Down
Loading