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