506 lines
20 KiB
C#
506 lines
20 KiB
C#
using System.Diagnostics;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Metadata;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Data;
|
|
using Avalonia.Input;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Layout;
|
|
using Avalonia.Utilities;
|
|
|
|
namespace Ursa.Controls;
|
|
|
|
/// <summary>
|
|
/// 1. Notice that this is not used in ScrollBar, so ViewportSize related feature is not necessary.
|
|
/// 2. Maximum, Minimum, MaxValue and MinValue are coerced there.
|
|
/// </summary>
|
|
[PseudoClasses(PC_Horizontal, PC_Vertical)]
|
|
public class RangeTrack: Control
|
|
{
|
|
public const string PC_Horizontal = ":horizontal";
|
|
public const string PC_Vertical = ":vertical";
|
|
private double _density;
|
|
private Vector _lastDrag;
|
|
|
|
private const double Tolerance = 0.0001;
|
|
|
|
public static readonly StyledProperty<double> MinimumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
|
nameof(Minimum), coerce: CoerceMinimum, defaultBindingMode:BindingMode.TwoWay);
|
|
|
|
public double Minimum
|
|
{
|
|
get => GetValue(MinimumProperty);
|
|
set => SetValue(MinimumProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<double> MaximumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
|
nameof(Maximum), coerce: CoerceMaximum, defaultBindingMode: BindingMode.TwoWay);
|
|
|
|
public double Maximum
|
|
{
|
|
get => GetValue(MaximumProperty);
|
|
set => SetValue(MaximumProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<double> LowerValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
|
nameof(LowerValue), coerce: CoerceLowerValue, defaultBindingMode: BindingMode.TwoWay);
|
|
|
|
public double LowerValue
|
|
{
|
|
get => GetValue(LowerValueProperty);
|
|
set => SetValue(LowerValueProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<double> UpperValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
|
nameof(UpperValue), coerce: CoerceUpperValue, defaultBindingMode: BindingMode.TwoWay);
|
|
|
|
public double UpperValue
|
|
{
|
|
get => GetValue(UpperValueProperty);
|
|
set => SetValue(UpperValueProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<Orientation> OrientationProperty = AvaloniaProperty.Register<RangeTrack, Orientation>(
|
|
nameof(Orientation));
|
|
|
|
public Orientation Orientation
|
|
{
|
|
get => GetValue(OrientationProperty);
|
|
set => SetValue(OrientationProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<Control?> UpperSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
|
nameof(UpperSection));
|
|
|
|
public Control? UpperSection
|
|
{
|
|
get => GetValue(UpperSectionProperty);
|
|
set => SetValue(UpperSectionProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<Control?> LowerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
|
nameof(LowerSection));
|
|
|
|
public Control? LowerSection
|
|
{
|
|
get => GetValue(LowerSectionProperty);
|
|
set => SetValue(LowerSectionProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<Control?> InnerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
|
nameof(InnerSection));
|
|
|
|
public Control? InnerSection
|
|
{
|
|
get => GetValue(InnerSectionProperty);
|
|
set => SetValue(InnerSectionProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<Control?> TrackBackgroundProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
|
nameof(TrackBackground));
|
|
|
|
public Control? TrackBackground
|
|
{
|
|
get => GetValue(TrackBackgroundProperty);
|
|
set => SetValue(TrackBackgroundProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<Thumb?> UpperThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
|
nameof(UpperThumb));
|
|
|
|
public Thumb? UpperThumb
|
|
{
|
|
get => GetValue(UpperThumbProperty);
|
|
set => SetValue(UpperThumbProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<Thumb?> LowerThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
|
nameof(LowerThumb));
|
|
|
|
public Thumb? LowerThumb
|
|
{
|
|
get => GetValue(LowerThumbProperty);
|
|
set => SetValue(LowerThumbProperty, value);
|
|
}
|
|
|
|
public static readonly StyledProperty<bool> IsDirectionReversedProperty = AvaloniaProperty.Register<RangeTrack, bool>(
|
|
nameof(IsDirectionReversed));
|
|
|
|
public bool IsDirectionReversed
|
|
{
|
|
get => GetValue(IsDirectionReversedProperty);
|
|
set => SetValue(IsDirectionReversedProperty, value);
|
|
}
|
|
|
|
public static readonly RoutedEvent<RangeValueChangedEventArgs> ValueChangedEvent =
|
|
RoutedEvent.Register<RangeTrack, RangeValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
|
|
|
public event EventHandler<RangeValueChangedEventArgs> ValueChanged
|
|
{
|
|
add => AddHandler(ValueChangedEvent, value);
|
|
remove => RemoveHandler(ValueChangedEvent, value);
|
|
}
|
|
|
|
static RangeTrack()
|
|
{
|
|
OrientationProperty.Changed.AddClassHandler<RangeTrack, Orientation>((o, e) => o.OnOrientationChanged(e));
|
|
LowerThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
|
UpperThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
|
LowerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
|
UpperSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
|
InnerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
|
MinimumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMinimumChanged(e));
|
|
MaximumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMaximumChanged(e));
|
|
LowerValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, true));
|
|
UpperValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, false));
|
|
AffectsArrange<RangeTrack>(
|
|
MinimumProperty,
|
|
MaximumProperty,
|
|
LowerValueProperty,
|
|
UpperValueProperty,
|
|
OrientationProperty,
|
|
IsDirectionReversedProperty);
|
|
}
|
|
|
|
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> args, bool isLower)
|
|
{
|
|
var oldValue = args.OldValue.Value;
|
|
var newValue = args.NewValue.Value;
|
|
if (Math.Abs(oldValue - newValue) > Tolerance)
|
|
{
|
|
RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower));
|
|
}
|
|
}
|
|
|
|
private void OnMinimumChanged(AvaloniaPropertyChangedEventArgs<double> avaloniaPropertyChangedEventArgs)
|
|
{
|
|
if (IsInitialized)
|
|
{
|
|
CoerceValue(MaximumProperty);
|
|
CoerceValue(LowerValueProperty);
|
|
CoerceValue(UpperValueProperty);
|
|
}
|
|
}
|
|
|
|
private void OnMaximumChanged(AvaloniaPropertyChangedEventArgs<double> avaloniaPropertyChangedEventArgs)
|
|
{
|
|
if (IsInitialized)
|
|
{
|
|
CoerceValue(LowerValueProperty);
|
|
CoerceValue(UpperValueProperty);
|
|
}
|
|
}
|
|
|
|
private void OnSectionChanged(AvaloniaPropertyChangedEventArgs<Control?> args)
|
|
{
|
|
var oldSection = args.OldValue.Value;
|
|
var newSection = args.NewValue.Value;
|
|
if (oldSection is not null)
|
|
{
|
|
LogicalChildren.Remove(oldSection);
|
|
VisualChildren.Remove(oldSection);
|
|
}
|
|
if (newSection is not null)
|
|
{
|
|
LogicalChildren.Add(newSection);
|
|
VisualChildren.Add(newSection);
|
|
}
|
|
}
|
|
|
|
private void OnThumbChanged(AvaloniaPropertyChangedEventArgs<Thumb?> args)
|
|
{
|
|
var oldThumb = args.OldValue.Value;
|
|
var newThumb = args.NewValue.Value;
|
|
if(oldThumb is not null)
|
|
{
|
|
LogicalChildren.Remove(oldThumb);
|
|
VisualChildren.Remove(oldThumb);
|
|
}
|
|
if (newThumb is not null)
|
|
{
|
|
newThumb.ZIndex = 5;
|
|
LogicalChildren.Add(newThumb);
|
|
VisualChildren.Add(newThumb);
|
|
}
|
|
}
|
|
|
|
private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs<Orientation> args)
|
|
{
|
|
Orientation o = args.NewValue.Value;
|
|
PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal);
|
|
PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical);
|
|
}
|
|
|
|
private static double CoerceMaximum(AvaloniaObject sender, double value)
|
|
{
|
|
return ValidateDouble(value)
|
|
? Math.Max(value, sender.GetValue(MinimumProperty))
|
|
: sender.GetValue(MaximumProperty);
|
|
}
|
|
|
|
private static double CoerceMinimum(AvaloniaObject sender, double value)
|
|
{
|
|
return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty);
|
|
}
|
|
|
|
private static double CoerceLowerValue(AvaloniaObject sender, double value)
|
|
{
|
|
if (!ValidateDouble(value)) return sender.GetValue(LowerValueProperty);
|
|
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty));
|
|
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(UpperValueProperty));
|
|
return value;
|
|
}
|
|
|
|
private static double CoerceUpperValue(AvaloniaObject sender, double value)
|
|
{
|
|
if (!ValidateDouble(value)) return sender.GetValue(UpperValueProperty);
|
|
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty));
|
|
value = MathUtilities.Clamp(value, sender.GetValue(LowerValueProperty), sender.GetValue(MaximumProperty));
|
|
return value;
|
|
}
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
base.OnInitialized();
|
|
CoerceValue(MaximumProperty);
|
|
CoerceValue(LowerValueProperty);
|
|
CoerceValue(UpperValueProperty);
|
|
}
|
|
|
|
protected override Size MeasureOverride(Size availableSize)
|
|
{
|
|
var desiredSize = new Size();
|
|
if (LowerThumb is not null && UpperThumb is not null)
|
|
{
|
|
LowerThumb.Measure(availableSize);
|
|
UpperThumb.Measure(availableSize);
|
|
if (Orientation == Orientation.Horizontal)
|
|
{
|
|
desiredSize = new Size(LowerThumb.DesiredSize.Width + UpperThumb.DesiredSize.Width,
|
|
Math.Max(LowerThumb.DesiredSize.Height, UpperThumb.DesiredSize.Height));
|
|
}
|
|
else
|
|
{
|
|
desiredSize = new Size(Math.Max(LowerThumb.DesiredSize.Width, UpperThumb.DesiredSize.Width),
|
|
LowerThumb.DesiredSize.Height + UpperThumb.DesiredSize.Height);
|
|
}
|
|
}
|
|
return desiredSize;
|
|
}
|
|
|
|
protected override Size ArrangeOverride(Size finalSize)
|
|
{
|
|
var vertical = Orientation == Orientation.Vertical;
|
|
double lowerButtonLength, innerButtonLength, upperButtonLength, lowerThumbLength, upperThumbLength;
|
|
ComputeSliderLengths(finalSize, vertical, out lowerButtonLength, out innerButtonLength, out upperButtonLength,
|
|
out lowerThumbLength, out upperThumbLength);
|
|
var offset = new Point();
|
|
var pieceSize = finalSize;
|
|
if (vertical)
|
|
{
|
|
CoerceLength(ref lowerButtonLength, finalSize.Height);
|
|
CoerceLength(ref innerButtonLength, finalSize.Height);
|
|
CoerceLength(ref upperButtonLength, finalSize.Height);
|
|
CoerceLength(ref lowerThumbLength, finalSize.Height);
|
|
CoerceLength(ref upperThumbLength, finalSize.Height);
|
|
if (IsDirectionReversed)
|
|
{
|
|
offset = offset.WithY(lowerThumbLength * 0.5);
|
|
pieceSize = pieceSize.WithHeight(lowerButtonLength);
|
|
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithY(offset.Y + lowerButtonLength);
|
|
pieceSize = pieceSize.WithHeight(innerButtonLength);
|
|
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithY(offset.Y + innerButtonLength);
|
|
pieceSize = pieceSize.WithHeight(upperButtonLength);
|
|
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithY(lowerButtonLength);
|
|
pieceSize = pieceSize.WithHeight(lowerThumbLength);
|
|
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithY(lowerButtonLength + innerButtonLength);
|
|
pieceSize = pieceSize.WithHeight(upperThumbLength);
|
|
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
|
}
|
|
else
|
|
{
|
|
offset = offset.WithY(upperThumbLength * 0.5);
|
|
pieceSize = pieceSize.WithHeight(upperButtonLength);
|
|
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithY(offset.Y + upperButtonLength);
|
|
pieceSize = pieceSize.WithHeight(innerButtonLength);
|
|
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithY(offset.Y + innerButtonLength);
|
|
pieceSize = pieceSize.WithHeight(lowerButtonLength);
|
|
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithY(upperButtonLength);
|
|
pieceSize = pieceSize.WithHeight(upperThumbLength);
|
|
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithY(upperButtonLength + innerButtonLength);
|
|
pieceSize = pieceSize.WithHeight(lowerThumbLength);
|
|
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CoerceLength(ref lowerButtonLength, finalSize.Width);
|
|
CoerceLength(ref innerButtonLength, finalSize.Width);
|
|
CoerceLength(ref upperButtonLength, finalSize.Width);
|
|
CoerceLength(ref lowerThumbLength, finalSize.Width);
|
|
CoerceLength(ref upperThumbLength, finalSize.Width);
|
|
if (IsDirectionReversed)
|
|
{
|
|
offset = offset.WithX(upperThumbLength * 0.5);
|
|
pieceSize = pieceSize.WithWidth(upperButtonLength);
|
|
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithX(offset.X + upperButtonLength);
|
|
pieceSize = pieceSize.WithWidth(innerButtonLength);
|
|
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithX(offset.X + innerButtonLength);
|
|
pieceSize = pieceSize.WithWidth(lowerButtonLength);
|
|
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithX(upperButtonLength);
|
|
pieceSize = pieceSize.WithWidth(upperThumbLength);
|
|
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithX(upperButtonLength+innerButtonLength);
|
|
pieceSize = pieceSize.WithWidth(lowerThumbLength);
|
|
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
|
}
|
|
else
|
|
{
|
|
offset = offset.WithX(lowerThumbLength * 0.5);
|
|
pieceSize = pieceSize.WithWidth(lowerButtonLength);
|
|
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithX(offset.X + lowerButtonLength);
|
|
pieceSize = pieceSize.WithWidth(innerButtonLength);
|
|
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
|
offset = offset.WithX(offset.X + innerButtonLength);
|
|
pieceSize = pieceSize.WithWidth(upperButtonLength);
|
|
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithX(lowerButtonLength);
|
|
pieceSize = pieceSize.WithWidth(lowerThumbLength);
|
|
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
offset = offset.WithX(lowerButtonLength + innerButtonLength);
|
|
pieceSize = pieceSize.WithWidth(upperThumbLength);
|
|
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
|
|
|
}
|
|
}
|
|
return finalSize;
|
|
}
|
|
|
|
private void ComputeSliderLengths(
|
|
Size arrangeSize,
|
|
bool isVertical,
|
|
out double lowerButtonLength,
|
|
out double innerButtonLength,
|
|
out double upperButtonLength,
|
|
out double lowerThumbLength,
|
|
out double upperThumbLength)
|
|
{
|
|
double range = Math.Max(0, Maximum - Minimum);
|
|
range += double.Epsilon;
|
|
double lowerOffset = Math.Min(range, LowerValue - Minimum);
|
|
double upperOffset = Math.Min(range, UpperValue - Minimum);
|
|
|
|
double trackLength;
|
|
if (isVertical)
|
|
{
|
|
trackLength = arrangeSize.Height;
|
|
lowerThumbLength = LowerThumb?.DesiredSize.Height ?? 0;
|
|
upperThumbLength = UpperThumb?.DesiredSize.Height ?? 0;
|
|
}
|
|
else
|
|
{
|
|
trackLength = arrangeSize.Width;
|
|
lowerThumbLength = LowerThumb?.DesiredSize.Width ?? 0;
|
|
upperThumbLength = UpperThumb?.DesiredSize.Width ?? 0;
|
|
}
|
|
|
|
CoerceLength(ref lowerThumbLength, trackLength);
|
|
CoerceLength(ref upperThumbLength, trackLength);
|
|
|
|
double remainingLength = trackLength - lowerThumbLength * 0.5 - upperThumbLength * 0.5;
|
|
|
|
lowerButtonLength = remainingLength * lowerOffset / range;
|
|
upperButtonLength = remainingLength * (range-upperOffset) / range;
|
|
innerButtonLength = remainingLength - lowerButtonLength - upperButtonLength;
|
|
|
|
_density = range / remainingLength;
|
|
}
|
|
|
|
private static void CoerceLength(ref double componentLength, double trackLength)
|
|
{
|
|
if (componentLength < 0)
|
|
{
|
|
componentLength = 0.0;
|
|
}
|
|
else if (componentLength > trackLength || double.IsNaN(componentLength))
|
|
{
|
|
componentLength = trackLength;
|
|
}
|
|
}
|
|
|
|
private static bool ValidateDouble(double value)
|
|
{
|
|
return !double.IsInfinity(value) && !double.IsNaN(value);
|
|
}
|
|
|
|
internal double GetRatioByPoint(double position)
|
|
{
|
|
bool isHorizontal = Orientation == Orientation.Horizontal;
|
|
var range = isHorizontal?
|
|
LowerSection?.Bounds.Width + InnerSection?.Bounds.Width + UpperSection?.Bounds.Width ?? double.Epsilon
|
|
: LowerSection?.Bounds.Height + InnerSection?.Bounds.Height + UpperSection?.Bounds.Height ?? double.Epsilon;
|
|
if (isHorizontal)
|
|
{
|
|
if (IsDirectionReversed)
|
|
{
|
|
double trackStart = UpperThumb?.Bounds.Width/2 ?? 0;
|
|
double trackEnd = trackStart + range;
|
|
if (position < trackStart) return 1.0;
|
|
if (position > trackEnd) return 0.0;
|
|
double diff = trackEnd - position;
|
|
return diff / range;
|
|
}
|
|
else
|
|
{
|
|
double trackStart = LowerThumb?.Bounds.Width/2 ?? 0;
|
|
double trackEnd = trackStart + range;
|
|
if (position < trackStart) return 0.0;
|
|
if (position > trackEnd) return 1.0;
|
|
double diff = position - trackStart;
|
|
return diff / range;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (IsDirectionReversed)
|
|
{
|
|
double trackStart = LowerThumb?.Bounds.Height / 2 ?? 0;
|
|
double trackEnd = trackStart + range;
|
|
if (position < trackStart) return 0.0;
|
|
if (position > trackEnd) return 1.0;
|
|
double diff = position - trackStart;
|
|
return diff / range;
|
|
}
|
|
else
|
|
{
|
|
double trackStart = UpperThumb?.Bounds.Height / 2 ?? 0;
|
|
double trackEnd = trackStart + range;
|
|
if (position < trackStart) return 1.0;
|
|
if (position > trackEnd) return 0.0;
|
|
double diff = trackEnd - position;
|
|
return diff / range;
|
|
}
|
|
}
|
|
}
|
|
} |