using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; namespace Ursa.Controls; [TemplatePart(PART_Track, typeof(RangeTrack))] [PseudoClasses(PC_Horizontal, PC_Vertical, PC_Pressed)] public class RangeSlider: TemplatedControl { public const string PART_Track = "PART_Track"; private const string PC_Horizontal= ":horizontal"; private const string PC_Vertical = ":vertical"; private const string PC_Pressed = ":pressed"; private RangeTrack? _track; private bool _isDragging; private IDisposable? _pointerPressedDisposable; private IDisposable? _pointerMoveDisposable; private IDisposable? _pointerReleasedDisposable; private const double Tolerance = 0.0001; public static readonly StyledProperty MinimumProperty = RangeTrack.MinimumProperty.AddOwner(); public double Minimum { get => GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } public static readonly StyledProperty MaximumProperty = RangeTrack.MaximumProperty.AddOwner(); public double Maximum { get => GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } public static readonly StyledProperty LowerValueProperty = RangeTrack.LowerValueProperty.AddOwner(); public double LowerValue { get => GetValue(LowerValueProperty); set => SetValue(LowerValueProperty, value); } public static readonly StyledProperty UpperValueProperty = RangeTrack.UpperValueProperty.AddOwner(); public double UpperValue { get => GetValue(UpperValueProperty); set => SetValue(UpperValueProperty, value); } public static readonly StyledProperty TrackWidthProperty = AvaloniaProperty.Register( nameof(TrackWidth)); public double TrackWidth { get => GetValue(TrackWidthProperty); set => SetValue(TrackWidthProperty, value); } public static readonly StyledProperty OrientationProperty = RangeTrack.OrientationProperty.AddOwner(); public Orientation Orientation { get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); } public static readonly StyledProperty IsDirectionReversedProperty = RangeTrack.IsDirectionReversedProperty.AddOwner(); public bool IsDirectionReversed { get => GetValue(IsDirectionReversedProperty); set => SetValue(IsDirectionReversedProperty, value); } public static readonly StyledProperty TickFrequencyProperty = AvaloniaProperty.Register( nameof(TickFrequency)); public double TickFrequency { get => GetValue(TickFrequencyProperty); set => SetValue(TickFrequencyProperty, value); } public static readonly StyledProperty?> TicksProperty = TickBar.TicksProperty.AddOwner(); public AvaloniaList? Ticks { get => GetValue(TicksProperty); set => SetValue(TicksProperty, value); } public static readonly StyledProperty TickPlacementProperty = Slider.TickPlacementProperty.AddOwner(); public TickPlacement TickPlacement { get => GetValue(TickPlacementProperty); set => SetValue(TickPlacementProperty, value); } public static readonly StyledProperty IsSnapToTickProperty = AvaloniaProperty.Register( nameof(IsSnapToTick)); public bool IsSnapToTick { get => GetValue(IsSnapToTickProperty); set => SetValue(IsSnapToTickProperty, 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 RangeSlider() { PressedMixin.Attach(); FocusableProperty.OverrideDefaultValue(true); IsHitTestVisibleProperty.OverrideDefaultValue(true); OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); OrientationProperty.Changed.AddClassHandler((o,e)=>o.OnOrientationChanged(e)); MinimumProperty.OverrideDefaultValue(0); MaximumProperty.OverrideDefaultValue(100); LowerValueProperty.OverrideDefaultValue(0); UpperValueProperty.OverrideDefaultValue(100); LowerValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, true)); UpperValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, false)); } 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)); } } public RangeSlider() { UpdatePseudoClasses(Orientation); } private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs args) { var value = args.NewValue.Value; UpdatePseudoClasses(value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); _pointerMoveDisposable?.Dispose(); _pointerPressedDisposable?.Dispose(); _pointerReleasedDisposable?.Dispose(); _track = e.NameScope.Find(PART_Track); _pointerMoveDisposable = this.AddDisposableHandler(PointerMovedEvent, PointerMove, RoutingStrategies.Tunnel); _pointerPressedDisposable = this.AddDisposableHandler(PointerPressedEvent, PointerPress, RoutingStrategies.Tunnel); _pointerReleasedDisposable = this.AddDisposableHandler(PointerReleasedEvent, PointerRelease, RoutingStrategies.Tunnel); } private Thumb? _currentThumb; private void PointerPress(object sender, PointerPressedEventArgs e) { if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { var point = e.GetCurrentPoint(_track); _currentThumb = GetThumbByPoint(point); MoveToPoint(point); _isDragging = true; } } private void PointerMove(object sender, PointerEventArgs args) { if (!IsEnabled) { _isDragging = false; return; } if (_isDragging) { MoveToPoint(args.GetCurrentPoint(_track)); } } private void PointerRelease(object sender, PointerReleasedEventArgs e) { _isDragging = false; _currentThumb = null; } private void MoveToPoint(PointerPoint posOnTrack) { if (_track is null) return; var value = GetValueByPoint(posOnTrack); var thumb = GetThumbByPoint(posOnTrack); if (_currentThumb !=null && _currentThumb != thumb) return; if (thumb is null) return; if (thumb == _track.LowerThumb) { SetCurrentValue(LowerValueProperty, IsSnapToTick ? SnapToTick(value) : value); } else { SetCurrentValue(UpperValueProperty, IsSnapToTick ? SnapToTick(value) : value); } } private double SnapToTick(double value) { if (IsSnapToTick) { var previous = Minimum; var next = Maximum; var ticks = Ticks; if (ticks != null && ticks.Count > 0) { foreach (var tick in ticks) { if (MathUtilities.AreClose(tick, value)) { return value; } if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous)) { previous = tick; } else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next)) { next = tick; } } } else if (MathUtilities.GreaterThan(TickFrequency, 0.0)) { previous = Minimum + Math.Round((value - Minimum) / TickFrequency) * TickFrequency; next = Math.Min(Maximum, previous + TickFrequency); } value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous; } return value; } private Thumb? GetThumbByPoint(PointerPoint point) { var isHorizontal = Orientation == Orientation.Horizontal; var lowerThumbPosition = isHorizontal? _track?.LowerThumb?.Bounds.Center.X : _track?.LowerThumb?.Bounds.Center.Y; var upperThumbPosition = isHorizontal? _track?.UpperThumb?.Bounds.Center.X : _track?.UpperThumb?.Bounds.Center.Y; var pointerPosition = isHorizontal? point.Position.X : point.Position.Y; var lowerDistance = Math.Abs((lowerThumbPosition ?? 0) - pointerPosition); var upperDistance = Math.Abs((upperThumbPosition ?? 0) - pointerPosition); if (lowerDistance