Files
Bartłomiej Dach 64624795b0 Invert if to reduce nesting
Just stylistic.
2025-04-16 11:31:21 +02:00

278 lines
9.6 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Numerics;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osuTK.Input;
using Vector2 = osuTK.Vector2;
namespace osu.Framework.Graphics.UserInterface
{
public abstract partial class SliderBar<T> : Container, IHasCurrentValue<T>
where T : struct, INumber<T>, IMinMaxValue<T>
{
/// <summary>
/// Range padding reduces the range of movement a slider bar is allowed to have
/// while still receiving input in the padded region. This behavior is necessary
/// for finite-sized nubs and can not be achieved (currently) by existing
/// scene graph padding / margin functionality.
/// </summary>
public float RangePadding;
public float UsableWidth => DrawWidth - 2 * RangePadding;
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep;
private readonly BindableNumber<T> currentNumberInstantaneous;
/// <summary>
/// When set, value changes based on user input are only transferred to any bound <see cref="Current"/> on commit.
/// 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.
/// </summary>
public bool TransferValueOnCommit;
private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<T>();
protected BindableNumber<T> CurrentNumber => current;
public Bindable<T> Current
{
get => current;
set
{
ArgumentNullException.ThrowIfNull(value);
current.Current = value;
currentNumberInstantaneous.Default = current.Default;
}
}
protected SliderBar()
{
currentNumberInstantaneous = new BindableNumber<T>();
current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue;
current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v;
current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v;
current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v;
current.DisabledChanged += disabled =>
{
if (disabled)
{
// revert any changes before disabling to make sure we are in a consistent state.
currentNumberInstantaneous.Value = current.Value;
uncommittedChanges = false;
}
currentNumberInstantaneous.Disabled = disabled;
};
currentNumberInstantaneous.ValueChanged += e =>
{
if (!TransferValueOnCommit)
current.Value = e.NewValue;
};
}
protected float NormalizedValue
{
get
{
if (!currentNumberInstantaneous.HasDefinedRange)
{
throw new InvalidOperationException($"A {nameof(SliderBar<T>)}'s {nameof(Current)} must have user-defined {nameof(BindableNumber<T>.MinValue)}"
+ $" and {nameof(BindableNumber<T>.MaxValue)} to produce a valid {nameof(NormalizedValue)}.");
}
return currentNumberInstantaneous.NormalizedValue;
}
}
/// <summary>
/// Triggered when the <see cref="Current"/> value has changed. Used to update the displayed value.
/// </summary>
/// <param name="value">The normalized <see cref="Current"/> value.</param>
protected abstract void UpdateValue(float value);
protected override void LoadComplete()
{
base.LoadComplete();
currentNumberInstantaneous.ValueChanged += _ => Scheduler.AddOnce(updateValue);
currentNumberInstantaneous.MinValueChanged += _ => Scheduler.AddOnce(updateValue);
currentNumberInstantaneous.MaxValueChanged += _ => Scheduler.AddOnce(updateValue);
Scheduler.AddOnce(updateValue);
}
private void updateValue() => UpdateValue(NormalizedValue);
private bool handleClick;
private float? relativeValueAtMouseDown;
protected override bool OnMouseDown(MouseDownEvent e)
{
// Relative value at MouseDown shouldn't change until dragging ends.
if (IsDragged)
return base.OnMouseDown(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);
// 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.
handleClick = false;
}
else
{
handleClick = true;
relativeValueAtMouseDown = null;
}
return base.OnMouseDown(e);
}
protected override bool OnClick(ClickEvent e)
{
if (handleClick)
{
handleMouseInput(e);
Commit();
}
return true;
}
protected override void OnDrag(DragEvent e)
{
handleMouseInput(e);
}
protected override bool OnDragStart(DragStartEvent e)
{
Vector2 posDiff = e.MouseDownPosition - e.MousePosition;
if (Math.Abs(posDiff.X) < Math.Abs(posDiff.Y))
{
handleClick = false;
return false;
}
GetContainingFocusManager()?.ChangeFocus(this);
handleMouseInput(e);
return true;
}
protected override void OnDragEnd(DragEndEvent e)
{
Commit();
}
public override bool AcceptsFocus => true;
protected override bool OnKeyDown(KeyDownEvent e)
{
if (currentNumberInstantaneous.Disabled)
return false;
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);
switch (e.Key)
{
case Key.Right:
currentNumberInstantaneous.Add(step);
onUserChange(currentNumberInstantaneous.Value);
return true;
case Key.Left:
currentNumberInstantaneous.Add(-step);
onUserChange(currentNumberInstantaneous.Value);
return true;
default:
return false;
}
}
protected override void OnKeyUp(KeyUpEvent e)
{
if (e.Key == Key.Left || e.Key == Key.Right)
Commit();
}
private bool uncommittedChanges;
protected virtual bool Commit()
{
if (!uncommittedChanges)
return false;
current.Value = currentNumberInstantaneous.Value;
uncommittedChanges = false;
return true;
}
/// <summary>
/// Whether mouse handling should be relative to the distance travelled, or absolute in line with the exact position of the cursor.
/// </summary>
/// <remarks>
/// Generally, this should be overridden and return <c>true</c> when the cursor is hovering a "nub" or "thumb" portion at the point of mouse down
/// to give the user more correct control.
/// </remarks>
/// <param name="e">The mouse down event.</param>
/// <returns>Whether to perform a relative drag.</returns>
protected virtual bool ShouldHandleAsRelativeDrag(MouseDownEvent e) => false;
private void handleMouseInput(MouseButtonEvent e)
{
if (currentNumberInstantaneous.Disabled)
return;
float localX = ToLocalSpace(e.ScreenSpaceMousePosition).X;
float newValue;
if (relativeValueAtMouseDown != null && e is DragEvent drag)
{
newValue = relativeValueAtMouseDown.Value + (localX - ToLocalSpace(drag.ScreenSpaceMouseDownPosition).X) / UsableWidth;
}
else
{
newValue = (localX - RangePadding) / UsableWidth;
}
currentNumberInstantaneous.SetProportional(newValue, e.ShiftPressed ? KeyboardStep : 0);
onUserChange(currentNumberInstantaneous.Value);
}
private void onUserChange(T value)
{
uncommittedChanges = true;
OnUserChange(value);
}
/// <summary>
/// Triggered when the value is changed based on end-user input to this control.
/// </summary>
protected virtual void OnUserChange(T value)
{
}
}
}