diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index b3e2753..5d9000b 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -17,6 +17,7 @@ public static class MenuKeys public const string MenuKeyNavigation = "Navigation"; public const string MenuKeyNumericUpDown = "NumericUpDown"; public const string MenuKeyPagination = "Pagination"; + public const string MenuKeyRangeSlider = "RangeSlider"; public const string MenuKeyTagInput = "TagInput"; public const string MenuKeyTimeline = "Timeline"; diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml new file mode 100644 index 0000000..7aa427b --- /dev/null +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs new file mode 100644 index 0000000..fe3181d --- /dev/null +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ursa.Demo.ViewModels; + +namespace Ursa.Demo.Pages; + +public partial class RangeSliderDemo : UserControl +{ + public RangeSliderDemo() + { + InitializeComponent(); + this.DataContext = new RangeSliderDemoViewModel(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 85312ef..fbcee7c 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -39,6 +39,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(), MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(), MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(), + MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(), MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(), MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(), }; diff --git a/demo/Ursa.Demo/ViewModels/MenuItemViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuItemViewModel.cs index 42be205..279fb52 100644 --- a/demo/Ursa.Demo/ViewModels/MenuItemViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuItemViewModel.cs @@ -5,11 +5,19 @@ using CommunityToolkit.Mvvm.Messaging; namespace Ursa.Demo.ViewModels; +public enum ControlStatus +{ + New, + Beta, + Stable, +} + public class MenuItemViewModel: ViewModelBase { public string MenuHeader { get; set; } public string MenuIconName { get; set; } public string Key { get; set; } + public string Status { get; set; } public bool IsSeparator { get; set; } public ObservableCollection Children { get; set; } = new(); diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index ea66a5e..a938dc4 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -22,12 +22,13 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox }, new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput }, new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading }, - new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox }, - new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation }, - new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown }, + new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox, Status = "New" }, + new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation, Status = "WIP" }, + new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" }, new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination }, + new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"}, new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput }, - new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline }, + new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" }, }; } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs new file mode 100644 index 0000000..9e8fd67 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using Avalonia.Layout; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public partial class RangeSliderDemoViewModel: ObservableObject +{ + public ObservableCollection Orientations { get; set; } = new ObservableCollection() + { + Orientation.Horizontal, + Orientation.Vertical + }; + + [ObservableProperty] private Orientation _orientation; +} \ No newline at end of file diff --git a/demo/Ursa.Demo/Views/MainView.axaml b/demo/Ursa.Demo/Views/MainView.axaml index 710026c..be08201 100644 --- a/demo/Ursa.Demo/Views/MainView.axaml +++ b/demo/Ursa.Demo/Views/MainView.axaml @@ -43,8 +43,21 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index edf9abb..3417acc 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -15,6 +15,7 @@ + diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs new file mode 100644 index 0000000..92f8298 --- /dev/null +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -0,0 +1,304 @@ +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 +/// 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; + } + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs b/src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs new file mode 100644 index 0000000..2effee1 --- /dev/null +++ b/src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs @@ -0,0 +1,34 @@ +using Avalonia.Interactivity; + +namespace Ursa.Controls; + +public class RangeValueChangedEventArgs: RoutedEventArgs +{ + public double OldValue { get; set; } + public double NewValue { get; set; } + public bool IsLower { get; set; } + + public RangeValueChangedEventArgs( + RoutedEvent routedEvent, + object source, + double oldValue, + double newValue, + bool isLower = true) : base(routedEvent, source) + { + OldValue = oldValue; + NewValue = newValue; + IsLower = isLower; + } + + public RangeValueChangedEventArgs( + RoutedEvent routedEvent, + double oldValue, + double newValue, + bool isLower = true) : base(routedEvent) + { + OldValue = oldValue; + NewValue = newValue; + IsLower = isLower; + } + +} \ No newline at end of file