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..d39cdad
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml
@@ -0,0 +1,12 @@
+
+
+
+
+
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/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index ea66a5e..4988aad 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -26,6 +26,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation },
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown },
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
+ new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider },
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
};
diff --git a/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs
new file mode 100644
index 0000000..6c4635c
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs
@@ -0,0 +1,8 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace Ursa.Demo.ViewModels;
+
+public class RangeSliderDemoViewModel: ObservableObject
+{
+
+}
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml
new file mode 100644
index 0000000..ac6f378
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
index 57f0c1e..1081737 100644
--- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs
+++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs
@@ -2,7 +2,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
-namespace Ursa.Controls.RangeSlider;
+namespace Ursa.Controls;
[TemplatePart(PART_DecreaseButton, typeof(Button))]
[TemplatePart(PART_IncreaseButton, typeof(Button))]
diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs
index 256b42c..8fe1773 100644
--- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs
+++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs
@@ -1,19 +1,29 @@
+using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Interactivity;
using Avalonia.Layout;
+using Avalonia.Utilities;
-namespace Ursa.Controls.RangeSlider;
+namespace Ursa.Controls;
///
-/// Notice that this is not used in ScrollBar, so ViewportSize related feature is not necessary.
+/// 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));
+ nameof(Minimum), coerce: CoerceMinimum);
public double Minimum
{
@@ -22,7 +32,7 @@ public class RangeTrack: Control
}
public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register(
- nameof(Maximum));
+ nameof(Maximum), coerce: CoerceMaximum);
public double Maximum
{
@@ -31,7 +41,7 @@ public class RangeTrack: Control
}
public static readonly StyledProperty LowerValueProperty = AvaloniaProperty.Register(
- nameof(LowerValue));
+ nameof(LowerValue), coerce: CoerceLowerValue);
public double LowerValue
{
@@ -40,7 +50,7 @@ public class RangeTrack: Control
}
public static readonly StyledProperty UpperValueProperty = AvaloniaProperty.Register(
- nameof(UpperValue));
+ nameof(UpperValue), coerce: CoerceUpperValue);
public double UpperValue
{
@@ -110,17 +120,168 @@ public class RangeTrack: Control
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));
- AffectsArrange(MinimumProperty, MaximumProperty, LowerValueProperty, UpperValueProperty, OrientationProperty, IsDirectionReversedProperty);
+ LowerThumbProperty.Changed.AddClassHandler((o, e) => o.OnThumbChanged(e));
+ UpperThumbProperty.Changed.AddClassHandler((o, e) => o.OnThumbChanged(e));
+ LowerButtonProperty.Changed.AddClassHandler((o, e) => o.OnButtonChanged(e));
+ UpperButtonProperty.Changed.AddClassHandler((o, e) => o.OnButtonChanged(e));
+ InnerButtonProperty.Changed.AddClassHandler((o, e) => o.OnButtonChanged(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 OnButtonChanged(AvaloniaPropertyChangedEventArgs