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; /// /// 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. /// [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 MinimumProperty = AvaloniaProperty.Register( nameof(Minimum), coerce: CoerceMinimum, defaultBindingMode:BindingMode.TwoWay); public double Minimum { get => GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register( nameof(Maximum), coerce: CoerceMaximum, defaultBindingMode: BindingMode.TwoWay); public double Maximum { get => GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } public static readonly StyledProperty LowerValueProperty = AvaloniaProperty.Register( nameof(LowerValue), coerce: CoerceLowerValue, defaultBindingMode: BindingMode.TwoWay); public double LowerValue { get => GetValue(LowerValueProperty); set => SetValue(LowerValueProperty, value); } public static readonly StyledProperty UpperValueProperty = AvaloniaProperty.Register( nameof(UpperValue), coerce: CoerceUpperValue, defaultBindingMode: BindingMode.TwoWay); public double UpperValue { get => GetValue(UpperValueProperty); set => SetValue(UpperValueProperty, value); } public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register( nameof(Orientation)); public Orientation Orientation { get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } public static readonly StyledProperty UpperSectionProperty = AvaloniaProperty.Register( nameof(UpperSection)); public Control? UpperSection { get => GetValue(UpperSectionProperty); set => SetValue(UpperSectionProperty, value); } public static readonly StyledProperty LowerSectionProperty = AvaloniaProperty.Register( nameof(LowerSection)); public Control? LowerSection { get => GetValue(LowerSectionProperty); set => SetValue(LowerSectionProperty, value); } public static readonly StyledProperty InnerSectionProperty = AvaloniaProperty.Register( nameof(InnerSection)); public Control? InnerSection { get => GetValue(InnerSectionProperty); set => SetValue(InnerSectionProperty, value); } public static readonly StyledProperty TrackBackgroundProperty = AvaloniaProperty.Register( nameof(TrackBackground)); public Control? TrackBackground { get => GetValue(TrackBackgroundProperty); set => SetValue(TrackBackgroundProperty, value); } public static readonly StyledProperty UpperThumbProperty = AvaloniaProperty.Register( nameof(UpperThumb)); public Thumb? UpperThumb { get => GetValue(UpperThumbProperty); set => SetValue(UpperThumbProperty, value); } public static readonly StyledProperty LowerThumbProperty = AvaloniaProperty.Register( nameof(LowerThumb)); public Thumb? LowerThumb { get => GetValue(LowerThumbProperty); set => SetValue(LowerThumbProperty, value); } public static readonly StyledProperty IsDirectionReversedProperty = AvaloniaProperty.Register( nameof(IsDirectionReversed)); public bool IsDirectionReversed { get => GetValue(IsDirectionReversedProperty); set => SetValue(IsDirectionReversedProperty, value); } public static readonly RoutedEvent ValueChangedEvent = RoutedEvent.Register(nameof(ValueChanged), RoutingStrategies.Bubble); public event EventHandler ValueChanged { add => AddHandler(ValueChangedEvent, value); remove => RemoveHandler(ValueChangedEvent, value); } static RangeTrack() { OrientationProperty.Changed.AddClassHandler((o, e) => o.OnOrientationChanged(e)); LowerThumbProperty.Changed.AddClassHandler((o, e) => o.OnThumbChanged(e)); UpperThumbProperty.Changed.AddClassHandler((o, e) => o.OnThumbChanged(e)); LowerSectionProperty.Changed.AddClassHandler((o, e) => o.OnSectionChanged(e)); UpperSectionProperty.Changed.AddClassHandler((o, e) => o.OnSectionChanged(e)); InnerSectionProperty.Changed.AddClassHandler((o, e) => o.OnSectionChanged(e)); MinimumProperty.Changed.AddClassHandler((o, e) => o.OnMinimumChanged(e)); MaximumProperty.Changed.AddClassHandler((o, e) => o.OnMaximumChanged(e)); LowerValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, true)); UpperValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, false)); AffectsArrange( MinimumProperty, MaximumProperty, LowerValueProperty, UpperValueProperty, OrientationProperty, IsDirectionReversedProperty); } private void OnValueChanged(AvaloniaPropertyChangedEventArgs 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 avaloniaPropertyChangedEventArgs) { if (IsInitialized) { CoerceValue(MaximumProperty); CoerceValue(LowerValueProperty); CoerceValue(UpperValueProperty); } } private void OnMaximumChanged(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs) { if (IsInitialized) { CoerceValue(LowerValueProperty); CoerceValue(UpperValueProperty); } } private void OnSectionChanged(AvaloniaPropertyChangedEventArgs 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 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 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; } } } }