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; 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 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 (oldValue != newValue) { 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) { oldThumb.DragDelta -= OnThumbDragDelta; LogicalChildren.Remove(oldThumb); VisualChildren.Remove(oldThumb); } if (newThumb is not null) { newThumb.DragDelta += OnThumbDragDelta; LogicalChildren.Add(newThumb); VisualChildren.Add(newThumb); } } private void OnThumbDragDelta(object sender, VectorEventArgs e) { return; if(sender is not Thumb thumb) return; bool lower = thumb == LowerThumb; double scale = IsDirectionReversed ? -1 : 1; double originalValue = lower ? LowerValue : UpperValue; double value; if (Orientation == Orientation.Horizontal) { value = scale * e.Vector.X * _density; } else { value = -1 * scale * e.Vector.Y * _density; } var factor = e.Vector / value; if (lower) { SetCurrentValue(LowerValueProperty, MathUtilities.Clamp(originalValue + value, Minimum, UpperValue)); } else { SetCurrentValue(UpperValueProperty, MathUtilities.Clamp(originalValue + value, LowerValue, Maximum)); } } 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(MinimumProperty); } private static double CoerceMinimum(AvaloniaObject sender, double value) { return ValidateDouble(value) ? value : sender.GetValue(MaximumProperty); } 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) { pieceSize = pieceSize.WithHeight(upperButtonLength); UpperSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + upperButtonLength); pieceSize = pieceSize.WithHeight(upperThumbLength); UpperThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + upperThumbLength); pieceSize = pieceSize.WithHeight(innerButtonLength); InnerSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + innerButtonLength); pieceSize = pieceSize.WithHeight(lowerThumbLength); LowerThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + lowerThumbLength); pieceSize = pieceSize.WithHeight(lowerButtonLength); LowerSection?.Arrange(new Rect(offset, pieceSize)); } else { pieceSize = pieceSize.WithHeight(lowerButtonLength); LowerSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + lowerButtonLength); pieceSize = pieceSize.WithHeight(lowerThumbLength); LowerThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + lowerThumbLength); pieceSize = pieceSize.WithHeight(innerButtonLength); InnerSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + innerButtonLength); pieceSize = pieceSize.WithHeight(upperThumbLength); UpperThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithY(offset.Y + upperThumbLength); pieceSize = pieceSize.WithHeight(upperButtonLength); UpperSection?.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) { pieceSize = pieceSize.WithWidth(upperButtonLength); UpperSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + upperButtonLength); pieceSize = pieceSize.WithWidth(upperThumbLength); UpperThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + upperThumbLength); pieceSize = pieceSize.WithWidth(innerButtonLength); InnerSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + innerButtonLength); pieceSize = pieceSize.WithWidth(lowerThumbLength); LowerThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + lowerThumbLength); pieceSize = pieceSize.WithWidth(lowerButtonLength); LowerSection?.Arrange(new Rect(offset, pieceSize)); } else { pieceSize = pieceSize.WithWidth(lowerButtonLength); LowerSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + lowerButtonLength); pieceSize = pieceSize.WithWidth(lowerThumbLength); LowerThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + lowerThumbLength); pieceSize = pieceSize.WithWidth(innerButtonLength); InnerSection?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + innerButtonLength); pieceSize = pieceSize.WithWidth(upperThumbLength); UpperThumb?.Arrange(new Rect(offset, pieceSize)); offset = offset.WithX(offset.X + upperThumbLength); pieceSize = pieceSize.WithWidth(upperButtonLength); UpperSection?.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); 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 - upperThumbLength; 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 GetThumbLength() { return Orientation == Orientation.Horizontal ? LowerThumb?.Bounds.Width ?? 0 + UpperThumb?.Bounds.Width ?? 0 : LowerThumb?.Bounds.Height ?? 0 + UpperThumb?.Bounds.Height ?? 0; } internal double GetTrackLength() { return Orientation == Orientation.Horizontal ? Bounds.Width : Bounds.Height; } }