From 13e30799a704dbe3da6a73d1951ab9d1db2ae3c8 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 17 Jan 2024 00:36:55 +0800 Subject: [PATCH 01/20] feat: init --- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 15 ++ src/Ursa/Controls/RangeSlider/RangeTrack.cs | 138 +++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 src/Ursa/Controls/RangeSlider/RangeSlider.cs create mode 100644 src/Ursa/Controls/RangeSlider/RangeTrack.cs diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs new file mode 100644 index 0000000..57f0c1e --- /dev/null +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -0,0 +1,15 @@ +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; + +namespace Ursa.Controls.RangeSlider; + +[TemplatePart(PART_DecreaseButton, typeof(Button))] +[TemplatePart(PART_IncreaseButton, typeof(Button))] +[TemplatePart(PART_Track, typeof(Track))] +public class RangeSlider: TemplatedControl +{ + public const string PART_DecreaseButton = "PART_DecreaseButton"; + public const string PART_IncreaseButton = "PART_IncreaseButton"; + public const string PART_Track = "PART_Track"; +} \ No newline at end of file diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs new file mode 100644 index 0000000..5c65243 --- /dev/null +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -0,0 +1,138 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Layout; + +namespace Ursa.Controls.RangeSlider; + +/// +/// Notice that this is not used in ScrollBar, so ViewportSize related feature is not necessary. +/// +public class RangeTrack: Control +{ + public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register( + nameof(Minimum)); + + public double Minimum + { + get => GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); + } + + public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register( + nameof(Maximum)); + + public double Maximum + { + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + + public static readonly StyledProperty LowerValueProperty = AvaloniaProperty.Register( + nameof(LowerValue)); + + public double LowerValue + { + get => GetValue(LowerValueProperty); + set => SetValue(LowerValueProperty, value); + } + + public static readonly StyledProperty UpperValueProperty = AvaloniaProperty.Register( + nameof(UpperValue)); + + 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 UpperButtonProperty = AvaloniaProperty.Register( + nameof(UpperButton)); + + public RepeatButton UpperButton + { + get => GetValue(UpperButtonProperty); + set => SetValue(UpperButtonProperty, value); + } + + public static readonly StyledProperty LowerButtonProperty = AvaloniaProperty.Register( + nameof(LowerButton)); + + public Button? LowerButton + { + get => GetValue(LowerButtonProperty); + set => SetValue(LowerButtonProperty, value); + } + + public static readonly StyledProperty InnerButtonProperty = AvaloniaProperty.Register( + nameof(InnerButton)); + + public Button? InnerButton + { + get => GetValue(InnerButtonProperty); + set => SetValue(InnerButtonProperty, 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); + } + + static RangeTrack() + { + AffectsArrange(MinimumProperty, MaximumProperty, LowerValueProperty, UpperValueProperty, OrientationProperty, IsDirectionReversedProperty); + } + + protected override Size MeasureOverride(Size availableSize) + { + var desiredSize = new Size(0.0, 0.0); + 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; + } +} \ No newline at end of file From e619102cfb35ecf2b1030051997c662d0bb74d6d Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 17 Jan 2024 16:55:24 +0800 Subject: [PATCH 02/20] feat: length calculation. --- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 80 +++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index 5c65243..256b42c 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -10,6 +10,8 @@ namespace Ursa.Controls.RangeSlider; /// public class RangeTrack: Control { + private double _density; + public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register( nameof(Minimum)); @@ -55,10 +57,10 @@ public class RangeTrack: Control set => SetValue(OrientationProperty, value); } - public static readonly StyledProperty UpperButtonProperty = AvaloniaProperty.Register( + public static readonly StyledProperty UpperButtonProperty = AvaloniaProperty.Register( nameof(UpperButton)); - public RepeatButton UpperButton + public Button? UpperButton { get => GetValue(UpperButtonProperty); set => SetValue(UpperButtonProperty, value); @@ -111,12 +113,19 @@ public class RangeTrack: Control static RangeTrack() { + OrientationProperty.Changed.AddClassHandler((o, e) => o.OnOrientationChanged(e)); AffectsArrange(MinimumProperty, MaximumProperty, LowerValueProperty, UpperValueProperty, OrientationProperty, IsDirectionReversedProperty); } + private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs args) + { + Orientation o = args.NewValue.Value; + this.PseudoClasses.Set("", true); + } + protected override Size MeasureOverride(Size availableSize) { - var desiredSize = new Size(0.0, 0.0); + var desiredSize = new Size(); if (LowerThumb is not null && UpperThumb is not null) { LowerThumb.Measure(availableSize); @@ -132,7 +141,70 @@ public class RangeTrack: Control 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); + + return base.ArrangeOverride(finalSize); + } + + private void ComputeSliderLengths( + Size arrangeSize, + bool isVertical, + out double lowerButtonLength, + out double innerButtonLength, + out double upperButtonLength, + out double lowerThumbLength, + out double upperThumbLength) + { + + double min = Minimum; + double max = Maximum; + double all = Math.Max(0, max - min); + double lowerOffset = Math.Min(all, LowerValue - min); + double upperOffset = Math.Min(all, UpperValue - min); + + 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 / all; + upperButtonLength = remainingLength * upperOffset / all; + innerButtonLength = remainingLength - lowerButtonLength - upperButtonLength; + + _density = all / 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; + } + } } \ No newline at end of file From a43dedeb3d753868f06052e44b1ca0c4273863d3 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 17 Jan 2024 22:26:56 +0800 Subject: [PATCH 03/20] feat: implement arrangement and dragging --- demo/Ursa.Demo/Models/MenuKeys.cs | 1 + demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 12 + demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs | 15 + .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 1 + .../ViewModels/RangeSliderDemoViewModel.cs | 8 + .../Controls/RangeSlider.axaml | 40 +++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa/Controls/RangeSlider/RangeSlider.cs | 2 +- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 287 ++++++++++++++++-- .../RangeSlider/RangeValueChangedEventArgs.cs | 34 +++ 11 files changed, 382 insertions(+), 20 deletions(-) create mode 100644 demo/Ursa.Demo/Pages/RangeSliderDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/RangeSlider.axaml create mode 100644 src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs 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 args) + { + var oldButton = args.OldValue.Value; + var newButton = args.NewValue.Value; + if (oldButton is not null) + { + LogicalChildren.Remove(oldButton); + VisualChildren.Remove(oldButton); + } + if (newButton is not null) + { + LogicalChildren.Add(newButton); + VisualChildren.Add(newButton); + } + } + + 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) + { + 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; - this.PseudoClasses.Set("", true); + 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) @@ -150,8 +311,94 @@ public class RangeTrack: Control double lowerButtonLength, innerButtonLength, upperButtonLength, lowerThumbLength, upperThumbLength; ComputeSliderLengths(finalSize, vertical, out lowerButtonLength, out innerButtonLength, out upperButtonLength, out lowerThumbLength, out upperThumbLength); - - return base.ArrangeOverride(finalSize); + 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); + UpperButton?.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); + InnerButton?.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); + LowerButton?.Arrange(new Rect(offset, pieceSize)); + } + else + { + pieceSize = pieceSize.WithHeight(lowerButtonLength); + LowerButton?.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); + InnerButton?.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); + UpperButton?.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); + UpperButton?.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); + InnerButton?.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); + LowerButton?.Arrange(new Rect(offset, pieceSize)); + } + else + { + pieceSize = pieceSize.WithWidth(lowerButtonLength); + LowerButton?.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); + InnerButton?.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); + UpperButton?.Arrange(new Rect(offset, pieceSize)); + + } + } + return finalSize; } private void ComputeSliderLengths( @@ -163,12 +410,9 @@ public class RangeTrack: Control out double lowerThumbLength, out double upperThumbLength) { - - double min = Minimum; - double max = Maximum; - double all = Math.Max(0, max - min); - double lowerOffset = Math.Min(all, LowerValue - min); - double upperOffset = Math.Min(all, UpperValue - min); + 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) @@ -189,11 +433,11 @@ public class RangeTrack: Control double remainingLength = trackLength -lowerThumbLength - upperThumbLength; - lowerButtonLength = remainingLength * lowerOffset / all; - upperButtonLength = remainingLength * upperOffset / all; + lowerButtonLength = remainingLength * lowerOffset / range; + upperButtonLength = remainingLength * (range-upperOffset) / range; innerButtonLength = remainingLength - lowerButtonLength - upperButtonLength; - _density = all / remainingLength; + _density = range / remainingLength; } private static void CoerceLength(ref double componentLength, double trackLength) @@ -207,4 +451,9 @@ public class RangeTrack: Control componentLength = trackLength; } } + + private static bool ValidateDouble(double value) + { + return !double.IsInfinity(value) && !double.IsNaN(value); + } } \ 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 From e1b5a524f5131821105da05980eeb151e8142b94 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 17 Jan 2024 22:46:05 +0800 Subject: [PATCH 04/20] feat: template binding --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 11 +++++- .../Controls/RangeSlider.axaml | 6 ++- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 38 +++++++++++++++++++ src/Ursa/Controls/RangeSlider/RangeTrack.cs | 9 +++-- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index d39cdad..5a0576c 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -6,7 +6,16 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.RangeSliderDemo"> + + + - + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml index ac6f378..f71b008 100644 --- a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -5,7 +5,11 @@ - + diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs index 1081737..d989c4f 100644 --- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -1,3 +1,4 @@ +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -12,4 +13,41 @@ public class RangeSlider: TemplatedControl public const string PART_DecreaseButton = "PART_DecreaseButton"; public const string PART_IncreaseButton = "PART_IncreaseButton"; public const string PART_Track = "PART_Track"; + + 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); + } + + static RangeSlider() + { + MinimumProperty.OverrideDefaultValue(0); + MaximumProperty.OverrideDefaultValue(100); + LowerValueProperty.OverrideDefaultValue(0); + UpperValueProperty.OverrideDefaultValue(100); + } + } \ No newline at end of file diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index 8fe1773..c37afe1 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -3,6 +3,7 @@ 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; @@ -23,7 +24,7 @@ public class RangeTrack: Control private Vector _lastDrag; public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register( - nameof(Minimum), coerce: CoerceMinimum); + nameof(Minimum), coerce: CoerceMinimum, defaultBindingMode:BindingMode.TwoWay); public double Minimum { @@ -32,7 +33,7 @@ public class RangeTrack: Control } public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register( - nameof(Maximum), coerce: CoerceMaximum); + nameof(Maximum), coerce: CoerceMaximum, defaultBindingMode: BindingMode.TwoWay); public double Maximum { @@ -41,7 +42,7 @@ public class RangeTrack: Control } public static readonly StyledProperty LowerValueProperty = AvaloniaProperty.Register( - nameof(LowerValue), coerce: CoerceLowerValue); + nameof(LowerValue), coerce: CoerceLowerValue, defaultBindingMode: BindingMode.TwoWay); public double LowerValue { @@ -50,7 +51,7 @@ public class RangeTrack: Control } public static readonly StyledProperty UpperValueProperty = AvaloniaProperty.Register( - nameof(UpperValue), coerce: CoerceUpperValue); + nameof(UpperValue), coerce: CoerceUpperValue, defaultBindingMode: BindingMode.TwoWay); public double UpperValue { From 696058fe465eb08a64a3ef21adfe44776e88ad7e Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 18 Jan 2024 02:00:56 +0800 Subject: [PATCH 05/20] feat: implement dual dragging. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 2 +- .../Controls/RangeSlider.axaml | 68 ++++++----- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 113 +++++++++++++++++- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 95 ++++++++------- 4 files changed, 200 insertions(+), 78 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index 5a0576c..da47e62 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -12,7 +12,7 @@ - + diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml index f71b008..a2647c1 100644 --- a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -1,42 +1,48 @@ - - + + + - - - - + Maximum="{Binding Maximum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + Minimum="{Binding Minimum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" + UpperValue="{Binding UpperValue, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> + + + - - - - - - - + - - - + + + - - - - - - - + - - - + + + diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs index d989c4f..e6fe313 100644 --- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -1,18 +1,25 @@ using Avalonia; 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_DecreaseButton, typeof(Button))] -[TemplatePart(PART_IncreaseButton, typeof(Button))] -[TemplatePart(PART_Track, typeof(Track))] +[TemplatePart(PART_Track, typeof(RangeTrack))] public class RangeSlider: TemplatedControl { - public const string PART_DecreaseButton = "PART_DecreaseButton"; - public const string PART_IncreaseButton = "PART_IncreaseButton"; - public const string PART_Track = "PART_Track"; + public const string PART_Track = "PART_Track"; + + private RangeTrack? _track; + private bool _isDragging; + private IDisposable? _pointerPressedDisposable; + private IDisposable? _pointerMoveDisposable; + private IDisposable? _pointerReleasedDisposable; public static readonly StyledProperty MinimumProperty = RangeTrack.MinimumProperty.AddOwner(); public double Minimum @@ -42,12 +49,106 @@ public class RangeSlider: TemplatedControl 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); + } + static RangeSlider() { + PressedMixin.Attach(); + FocusableProperty.OverrideDefaultValue(true); + IsHitTestVisibleProperty.OverrideDefaultValue(true); + OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); MinimumProperty.OverrideDefaultValue(0); MaximumProperty.OverrideDefaultValue(100); LowerValueProperty.OverrideDefaultValue(0); UpperValueProperty.OverrideDefaultValue(100); } + + 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 void PointerPress(object sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + var point = e.GetCurrentPoint(_track); + 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; + } + + private void MoveToPoint(PointerPoint posOnTrack) + { + if (_track is null) return; + var isHorizontal = Orientation == Orientation.Horizontal; + var thumbLength = _track.GetThumbLength(); + var trackLength = _track.GetTrackLength() - thumbLength; + var pos = isHorizontal ? posOnTrack.Position.X : posOnTrack.Position.Y; + var lowerPosition = isHorizontal? _track.LowerThumb.Bounds.X : _track.LowerThumb.Bounds.Y; + var upperPosition = isHorizontal? _track.UpperThumb.Bounds.X : _track.UpperThumb.Bounds.Y; + bool lower = Math.Abs(pos - lowerPosition) < Math.Abs(pos - upperPosition); + var logicalPosition = MathUtilities.Clamp((pos - thumbLength*0.5) / trackLength, 0.0, 1.0); + var invert = isHorizontal ? IsDirectionReversed ? 1.0 : 0 : + IsDirectionReversed ? 0 : 1.0; + var calValue = Math.Abs(invert - logicalPosition); + var range = Maximum - Minimum; + var finalValue = calValue * range + Minimum; + SetCurrentValue(lower? LowerValueProperty: UpperValueProperty, finalValue); + } + + private double SnapToTick(double value) + { + return value; + } } \ No newline at end of file diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index c37afe1..12fc381 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -68,31 +68,31 @@ public class RangeTrack: Control set => SetValue(OrientationProperty, value); } - public static readonly StyledProperty UpperButtonProperty = AvaloniaProperty.Register( - nameof(UpperButton)); + public static readonly StyledProperty UpperSectionProperty = AvaloniaProperty.Register( + nameof(UpperSection)); - public Button? UpperButton + public Control? UpperSection { - get => GetValue(UpperButtonProperty); - set => SetValue(UpperButtonProperty, value); + get => GetValue(UpperSectionProperty); + set => SetValue(UpperSectionProperty, value); } - public static readonly StyledProperty LowerButtonProperty = AvaloniaProperty.Register( - nameof(LowerButton)); + public static readonly StyledProperty LowerSectionProperty = AvaloniaProperty.Register( + nameof(LowerSection)); - public Button? LowerButton + public Control? LowerSection { - get => GetValue(LowerButtonProperty); - set => SetValue(LowerButtonProperty, value); + get => GetValue(LowerSectionProperty); + set => SetValue(LowerSectionProperty, value); } - public static readonly StyledProperty InnerButtonProperty = AvaloniaProperty.Register( - nameof(InnerButton)); + public static readonly StyledProperty InnerSectionProperty = AvaloniaProperty.Register( + nameof(InnerSection)); - public Button? InnerButton + public Control? InnerSection { - get => GetValue(InnerButtonProperty); - set => SetValue(InnerButtonProperty, value); + get => GetValue(InnerSectionProperty); + set => SetValue(InnerSectionProperty, value); } public static readonly StyledProperty UpperThumbProperty = AvaloniaProperty.Register( @@ -136,9 +136,9 @@ public class RangeTrack: Control OrientationProperty.Changed.AddClassHandler((o, e) => o.OnOrientationChanged(e)); 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)); + 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)); @@ -181,19 +181,19 @@ public class RangeTrack: Control } } - private void OnButtonChanged(AvaloniaPropertyChangedEventArgs args) + private void OnSectionChanged(AvaloniaPropertyChangedEventArgs args) { - var oldButton = args.OldValue.Value; - var newButton = args.NewValue.Value; - if (oldButton is not null) + var oldSection = args.OldValue.Value; + var newSection = args.NewValue.Value; + if (oldSection is not null) { - LogicalChildren.Remove(oldButton); - VisualChildren.Remove(oldButton); + LogicalChildren.Remove(oldSection); + VisualChildren.Remove(oldSection); } - if (newButton is not null) + if (newSection is not null) { - LogicalChildren.Add(newButton); - VisualChildren.Add(newButton); + LogicalChildren.Add(newSection); + VisualChildren.Add(newSection); } } @@ -216,7 +216,8 @@ public class RangeTrack: Control } private void OnThumbDragDelta(object sender, VectorEventArgs e) - { + { + return; if(sender is not Thumb thumb) return; bool lower = thumb == LowerThumb; double scale = IsDirectionReversed ? -1 : 1; @@ -324,36 +325,36 @@ public class RangeTrack: Control if (IsDirectionReversed) { pieceSize = pieceSize.WithHeight(upperButtonLength); - UpperButton?.Arrange(new Rect(offset, pieceSize)); + 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); - InnerButton?.Arrange(new Rect(offset, pieceSize)); + 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); - LowerButton?.Arrange(new Rect(offset, pieceSize)); + LowerSection?.Arrange(new Rect(offset, pieceSize)); } else { pieceSize = pieceSize.WithHeight(lowerButtonLength); - LowerButton?.Arrange(new Rect(offset, pieceSize)); + 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); - InnerButton?.Arrange(new Rect(offset, pieceSize)); + 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); - UpperButton?.Arrange(new Rect(offset, pieceSize)); + UpperSection?.Arrange(new Rect(offset, pieceSize)); } } else @@ -366,36 +367,36 @@ public class RangeTrack: Control if (IsDirectionReversed) { pieceSize = pieceSize.WithWidth(upperButtonLength); - UpperButton?.Arrange(new Rect(offset, pieceSize)); + 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); - InnerButton?.Arrange(new Rect(offset, pieceSize)); + 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); - LowerButton?.Arrange(new Rect(offset, pieceSize)); + LowerSection?.Arrange(new Rect(offset, pieceSize)); } else { pieceSize = pieceSize.WithWidth(lowerButtonLength); - LowerButton?.Arrange(new Rect(offset, pieceSize)); + 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); - InnerButton?.Arrange(new Rect(offset, pieceSize)); + 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); - UpperButton?.Arrange(new Rect(offset, pieceSize)); + UpperSection?.Arrange(new Rect(offset, pieceSize)); } } @@ -457,4 +458,18 @@ public class RangeTrack: Control { 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; + } } \ No newline at end of file From 9057803cb46140eab89332b2d18b7c9b588d56e4 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 18 Jan 2024 23:43:44 +0800 Subject: [PATCH 06/20] feat: add thumb cache to avoid drag conflict. --- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 73 ++++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs index e6fe313..371e662 100644 --- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; @@ -100,11 +101,13 @@ public class RangeSlider: TemplatedControl } + 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; } @@ -126,29 +129,71 @@ public class RangeSlider: TemplatedControl private void PointerRelease(object sender, PointerReleasedEventArgs e) { _isDragging = false; + _currentThumb = null; } private void MoveToPoint(PointerPoint posOnTrack) { if (_track is null) return; - var isHorizontal = Orientation == Orientation.Horizontal; - var thumbLength = _track.GetThumbLength(); - var trackLength = _track.GetTrackLength() - thumbLength; - var pos = isHorizontal ? posOnTrack.Position.X : posOnTrack.Position.Y; - var lowerPosition = isHorizontal? _track.LowerThumb.Bounds.X : _track.LowerThumb.Bounds.Y; - var upperPosition = isHorizontal? _track.UpperThumb.Bounds.X : _track.UpperThumb.Bounds.Y; - bool lower = Math.Abs(pos - lowerPosition) < Math.Abs(pos - upperPosition); - var logicalPosition = MathUtilities.Clamp((pos - thumbLength*0.5) / trackLength, 0.0, 1.0); - var invert = isHorizontal ? IsDirectionReversed ? 1.0 : 0 : - IsDirectionReversed ? 0 : 1.0; - var calValue = Math.Abs(invert - logicalPosition); - var range = Maximum - Minimum; - var finalValue = calValue * range + Minimum; - SetCurrentValue(lower? LowerValueProperty: UpperValueProperty, finalValue); + 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, value); + } + else + { + SetCurrentValue(UpperValueProperty, value); + } } private double SnapToTick(double value) { return value; } + + private Thumb? GetThumbByPoint(PointerPoint point) + { + var isHorizontal = Orientation == Orientation.Horizontal; + var lowerThumbPosition = isHorizontal? _track?.LowerThumb?.Bounds.Position.X : _track?.LowerThumb?.Bounds.Position.Y; + var upperThumbPosition = isHorizontal? _track?.UpperThumb?.Bounds.Position.X : _track?.UpperThumb?.Bounds.Position.Y; + var thumbWidth = isHorizontal? _track?.LowerThumb?.Bounds.Width : _track?.LowerThumb?.Bounds.Height; + 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 trackLength - thumbLength) + return isHorizontal? Maximum : Minimum; + trackLength -= thumbLength * 2; + pointPosition = MathUtilities.Clamp(pointPosition / trackLength, 0.0, 1.0); + var invert = isHorizontal + ? IsDirectionReversed ? 1.0 : 0 + : IsDirectionReversed ? 0 : 1.0; + var calValue = Math.Abs(invert - pointPosition); + var range = Maximum - Minimum; + var finalValue = calValue * range + Minimum; + return finalValue; + } } \ No newline at end of file From 8c3004bb6d5cf1256ea7ad5e2256904b3ae5d239 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 00:22:16 +0800 Subject: [PATCH 07/20] feat: implement snap to tick. --- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 86 +++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs index 371e662..7da7cfa 100644 --- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using Avalonia; +using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Mixins; @@ -12,9 +13,12 @@ using Avalonia.Utilities; namespace Ursa.Controls; [TemplatePart(PART_Track, typeof(RangeTrack))] +[PseudoClasses(PC_Horizontal, PC_Vertical)] public class RangeSlider: TemplatedControl { public const string PART_Track = "PART_Track"; + private const string PC_Horizontal= "horizontal"; + private const string PC_Vertical = "vertical"; private RangeTrack? _track; private bool _isDragging; @@ -75,6 +79,42 @@ public class RangeSlider: TemplatedControl 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); + } static RangeSlider() { @@ -82,12 +122,20 @@ public class RangeSlider: TemplatedControl 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); } + private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs args) + { + var value = args.NewValue.Value; + PseudoClasses.Set(PC_Horizontal, value == Orientation.Horizontal); + PseudoClasses.Set(PC_Vertical, value == Orientation.Vertical); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -141,16 +189,50 @@ public class RangeSlider: TemplatedControl if (thumb is null) return; if (thumb == _track.LowerThumb) { - SetCurrentValue(LowerValueProperty, value); + SetCurrentValue(LowerValueProperty, IsSnapToTick ? SnapToTick(value) : value); } else { - SetCurrentValue(UpperValueProperty, value); + 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; } From 2d37fc84fb3c43a3dc15d2113dfdddf98aafde0f Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 00:41:19 +0800 Subject: [PATCH 08/20] feat: update demo. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 2 +- demo/Ursa.Demo/ViewModels/MenuItemViewModel.cs | 8 ++++++++ demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 +- demo/Ursa.Demo/Views/MainView.axaml | 9 ++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index da47e62..cc75ae9 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -12,7 +12,7 @@ - + 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 4988aad..18d670a 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -26,7 +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 = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "WIP"}, new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput }, new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline }, }; diff --git a/demo/Ursa.Demo/Views/MainView.axaml b/demo/Ursa.Demo/Views/MainView.axaml index 710026c..41e0164 100644 --- a/demo/Ursa.Demo/Views/MainView.axaml +++ b/demo/Ursa.Demo/Views/MainView.axaml @@ -43,8 +43,15 @@ + + + + + + + Date: Fri, 19 Jan 2024 17:48:07 +0800 Subject: [PATCH 09/20] fix: make thumb overlaying sections. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 2 +- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 75 ++++++++++++++------- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index cc75ae9..68f58eb 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -12,7 +12,7 @@ - + diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index 12fc381..4c666c8 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -94,6 +94,15 @@ public class RangeTrack: Control 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)); @@ -210,6 +219,7 @@ public class RangeTrack: Control if (newThumb is not null) { newThumb.DragDelta += OnThumbDragDelta; + newThumb.ZIndex = 5; LogicalChildren.Add(newThumb); VisualChildren.Add(newThumb); } @@ -324,37 +334,43 @@ public class RangeTrack: Control CoerceLength(ref upperThumbLength, finalSize.Height); if (IsDirectionReversed) { + 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(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)); + + 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 { + 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(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)); + + 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 @@ -366,37 +382,43 @@ public class RangeTrack: Control 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(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)); + + 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(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)); + + 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)); } } @@ -413,6 +435,7 @@ public class RangeTrack: Control 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); @@ -432,8 +455,8 @@ public class RangeTrack: Control CoerceLength(ref lowerThumbLength, trackLength); CoerceLength(ref upperThumbLength, trackLength); - - double remainingLength = trackLength -lowerThumbLength - upperThumbLength; + + double remainingLength = trackLength - lowerThumbLength * 0.5 - upperThumbLength * 0.5; lowerButtonLength = remainingLength * lowerOffset / range; upperButtonLength = remainingLength * (range-upperOffset) / range; From 236a44c62cd33318acfc136cfaac31b7cca916bf Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 18:28:05 +0800 Subject: [PATCH 10/20] feat: refactor length calculation. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 2 +- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 22 +++++---- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 49 ++++++++++++++++++++ 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index 68f58eb..cc75ae9 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -12,7 +12,7 @@ - + diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs index 7da7cfa..d04ca64 100644 --- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -239,9 +239,8 @@ public class RangeSlider: TemplatedControl private Thumb? GetThumbByPoint(PointerPoint point) { var isHorizontal = Orientation == Orientation.Horizontal; - var lowerThumbPosition = isHorizontal? _track?.LowerThumb?.Bounds.Position.X : _track?.LowerThumb?.Bounds.Position.Y; - var upperThumbPosition = isHorizontal? _track?.UpperThumb?.Bounds.Position.X : _track?.UpperThumb?.Bounds.Position.Y; - var thumbWidth = isHorizontal? _track?.LowerThumb?.Bounds.Width : _track?.LowerThumb?.Bounds.Height; + 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); @@ -261,21 +260,28 @@ public class RangeSlider: TemplatedControl { if (_track is null) return 0; var isHorizontal = Orientation == Orientation.Horizontal; - var trackLength = _track.GetTrackLength(); + var pointPosition = isHorizontal ? point.Position.X : point.Position.Y; + var ratio = _track.GetRatioByPoint(pointPosition); + var range = Maximum - Minimum; + var finalValue = ratio * range + Minimum; + return finalValue; + /* + var trackLength = _track.GetTrackLength(); var thumbLength = _track.GetThumbLength() * 0.5; - if(pointPosition < thumbLength) + if(pointPosition < thumbLength * 0.5) return isHorizontal? Minimum : Maximum; - if (pointPosition > trackLength - thumbLength) + if (pointPosition > trackLength - thumbLength * 0.5) return isHorizontal? Maximum : Minimum; trackLength -= thumbLength * 2; pointPosition = MathUtilities.Clamp(pointPosition / trackLength, 0.0, 1.0); - var invert = isHorizontal - ? IsDirectionReversed ? 1.0 : 0 + var invert = isHorizontal + ? IsDirectionReversed ? 1.0 : 0 : IsDirectionReversed ? 0 : 1.0; var calValue = Math.Abs(invert - pointPosition); var range = Maximum - Minimum; var finalValue = calValue * range + Minimum; return finalValue; + */ } } \ No newline at end of file diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index 4c666c8..355a28f 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -495,4 +495,53 @@ public class RangeTrack: Control ? Bounds.Width : Bounds.Height; } + + internal double GetRatioByPoint(double position) + { + bool isHorizontal = Orientation == Orientation.Horizontal; + var range = LowerSection?.Bounds.Width + InnerSection?.Bounds.Width + UpperSection?.Bounds.Width ?? 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 = 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; + } + else + { + 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; + } + } + } } \ No newline at end of file From 33543c7ebfa395a71f9a948bec28c6f775473f28 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 18:36:19 +0800 Subject: [PATCH 11/20] fix: fix vertical bug. --- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index 355a28f..aafabcd 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -499,10 +499,11 @@ public class RangeTrack: Control internal double GetRatioByPoint(double position) { bool isHorizontal = Orientation == Orientation.Horizontal; - var range = LowerSection?.Bounds.Width + InnerSection?.Bounds.Width + UpperSection?.Bounds.Width ?? double.Epsilon; + 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; From 48bdb2cfb957fd26505774ca6d6bce5a6ed94cf0 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 19:12:33 +0800 Subject: [PATCH 12/20] feat: enable tick visual. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 2 +- .../Controls/RangeSlider.axaml | 234 +++++++++++++++--- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 37 ++- 3 files changed, 209 insertions(+), 64 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index cc75ae9..aa5bcf6 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -12,7 +12,7 @@ - + diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml index a2647c1..54cb6f4 100644 --- a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -5,46 +5,198 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs index d04ca64..401a6bc 100644 --- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -17,8 +17,8 @@ namespace Ursa.Controls; 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_Horizontal= ":horizontal"; + private const string PC_Vertical = ":vertical"; private RangeTrack? _track; private bool _isDragging; @@ -128,12 +128,16 @@ public class RangeSlider: TemplatedControl LowerValueProperty.OverrideDefaultValue(0); UpperValueProperty.OverrideDefaultValue(100); } + + public RangeSlider() + { + UpdatePseudoClasses(Orientation); + } private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs args) { var value = args.NewValue.Value; - PseudoClasses.Set(PC_Horizontal, value == Orientation.Horizontal); - PseudoClasses.Set(PC_Vertical, value == Orientation.Vertical); + UpdatePseudoClasses(value); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -146,10 +150,10 @@ public class RangeSlider: TemplatedControl _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) @@ -266,22 +270,11 @@ public class RangeSlider: TemplatedControl var range = Maximum - Minimum; var finalValue = ratio * range + Minimum; return finalValue; - /* - var trackLength = _track.GetTrackLength(); - var thumbLength = _track.GetThumbLength() * 0.5; - if(pointPosition < thumbLength * 0.5) - return isHorizontal? Minimum : Maximum; - if (pointPosition > trackLength - thumbLength * 0.5) - return isHorizontal? Maximum : Minimum; - trackLength -= thumbLength * 2; - pointPosition = MathUtilities.Clamp(pointPosition / trackLength, 0.0, 1.0); - var invert = isHorizontal - ? IsDirectionReversed ? 1.0 : 0 - : IsDirectionReversed ? 0 : 1.0; - var calValue = Math.Abs(invert - pointPosition); - var range = Maximum - Minimum; - var finalValue = calValue * range + Minimum; - return finalValue; - */ + } + + private void UpdatePseudoClasses(Orientation o) + { + this.PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical); + this.PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal); } } \ No newline at end of file From c17f5cbfa3f877fc5c2ca0cb7cc4ee2ad71d30f8 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 22:32:26 +0800 Subject: [PATCH 13/20] feat: clean up code. add event to slider. --- src/Ursa/Controls/RangeSlider/RangeSlider.cs | 25 ++++++++++- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 44 -------------------- 2 files changed, 23 insertions(+), 46 deletions(-) diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs index 401a6bc..2bade27 100644 --- a/src/Ursa/Controls/RangeSlider/RangeSlider.cs +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -116,19 +116,40 @@ public class RangeSlider: TemplatedControl 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)); + 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 (oldValue != newValue) + { + RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower)); + } + } + public RangeSlider() { UpdatePseudoClasses(Orientation); diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index aafabcd..f3b80ed 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -212,47 +212,17 @@ public class RangeTrack: Control 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; newThumb.ZIndex = 5; 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; @@ -481,20 +451,6 @@ public class RangeTrack: Control { 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; - } internal double GetRatioByPoint(double position) { From 7cc8212e02be99a24506e980c727b2eb8f630104 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 22:46:20 +0800 Subject: [PATCH 14/20] feat: Update resources. --- .../Controls/RangeSlider.axaml | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml index 54cb6f4..f78bd15 100644 --- a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -4,7 +4,9 @@ xmlns:u="https://irihi.tech/ursa"> - + + + + + + + + + + From d84754079860927f846ebd1454551f445b577204 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 19 Jan 2024 23:30:17 +0800 Subject: [PATCH 15/20] feat: improve badge. --- demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 10 +++++----- demo/Ursa.Demo/Views/MainView.axaml | 14 ++++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 18d670a..a938dc4 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -22,13 +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 = "WIP"}, + 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/Views/MainView.axaml b/demo/Ursa.Demo/Views/MainView.axaml index 41e0164..be08201 100644 --- a/demo/Ursa.Demo/Views/MainView.axaml +++ b/demo/Ursa.Demo/Views/MainView.axaml @@ -43,13 +43,19 @@ - - - + + + + + From 63f82821e597f6803b9e718000a1f39bc7cf1564 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 20 Jan 2024 00:37:26 +0800 Subject: [PATCH 16/20] fix vertical issue, fix coerce typo. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 1 + src/Ursa.Themes.Semi/Controls/RangeSlider.axaml | 2 ++ src/Ursa/Controls/RangeSlider/RangeTrack.cs | 4 ++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index aa5bcf6..5f5412a 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -17,5 +17,6 @@ + diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml index f78bd15..529a94a 100644 --- a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -54,6 +54,7 @@ Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" + Orientation="{Binding Orientation, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" LowerValue="{Binding LowerValue, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" Maximum="{Binding Maximum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" Minimum="{Binding Minimum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" @@ -144,6 +145,7 @@ Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" + Orientation="{Binding Orientation, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" LowerValue="{Binding LowerValue, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" Maximum="{Binding Maximum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" Minimum="{Binding Minimum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index f3b80ed..7d2a48b 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -234,12 +234,12 @@ public class RangeTrack: Control { return ValidateDouble(value) ? Math.Max(value, sender.GetValue(MinimumProperty)) - : sender.GetValue(MinimumProperty); + : sender.GetValue(MaximumProperty); } private static double CoerceMinimum(AvaloniaObject sender, double value) { - return ValidateDouble(value) ? value : sender.GetValue(MaximumProperty); + return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty); } private static double CoerceLowerValue(AvaloniaObject sender, double value) From 28a8df285f94271c23f49585d95741e0420c5599 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 20 Jan 2024 01:36:02 +0800 Subject: [PATCH 17/20] fix: fix reapplying template initialization issue. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 8 ++++++-- .../ViewModels/RangeSliderDemoViewModel.cs | 12 +++++++++-- .../Controls/RangeSlider.axaml | 20 +++++++++---------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index 5f5412a..87ddecb 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -4,6 +4,9 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:u="https://irihi.tech/ursa" mc:Ignorable="d" d:DesignWidth="800" + xmlns:vm="using:Ursa.Demo.ViewModels" + x:DataType="vm:RangeSliderDemoViewModel" + x:CompileBindings="True" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.RangeSliderDemo"> @@ -12,11 +15,12 @@ - + - + + diff --git a/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs index 6c4635c..9e8fd67 100644 --- a/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs @@ -1,8 +1,16 @@ +using System.Collections.ObjectModel; +using Avalonia.Layout; using CommunityToolkit.Mvvm.ComponentModel; namespace Ursa.Demo.ViewModels; -public class RangeSliderDemoViewModel: ObservableObject +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/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml index 529a94a..1ab4c95 100644 --- a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -54,11 +54,11 @@ Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - Orientation="{Binding Orientation, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" - LowerValue="{Binding LowerValue, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" - Maximum="{Binding Maximum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" - Minimum="{Binding Minimum, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" - UpperValue="{Binding UpperValue, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"> + Orientation="{Binding Orientation, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + LowerValue="{Binding LowerValue, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + Maximum="{Binding Maximum, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + Minimum="{Binding Minimum, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + UpperValue="{Binding UpperValue, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"> + Orientation="{Binding Orientation, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + LowerValue="{Binding LowerValue, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + Maximum="{Binding Maximum, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + Minimum="{Binding Minimum, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" + UpperValue="{Binding UpperValue, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}"> Date: Sat, 20 Jan 2024 12:44:13 +0800 Subject: [PATCH 18/20] feat: change demo layout. --- demo/Ursa.Demo/Pages/RangeSliderDemo.axaml | 56 ++++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml index 87ddecb..7aa427b 100644 --- a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -1,26 +1,52 @@ - + - + - - + + + + + + + From d2f01c12572d9afce8170ff75852337d668387e7 Mon Sep 17 00:00:00 2001 From: Zhang Dian <54255897+zdpcdt@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:15:51 +0800 Subject: [PATCH 19/20] feat: :pressed, comparison. --- src/Ursa.Themes.Semi/Controls/RangeSlider.axaml | 2 ++ src/Ursa/Controls/RangeSlider/RangeSlider.cs | 7 +++++-- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 6 ++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml index 1ab4c95..7d66251 100644 --- a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -52,6 +52,7 @@ MinimumProperty = RangeTrack.MinimumProperty.AddOwner(); public double Minimum { @@ -144,7 +147,7 @@ public class RangeSlider: TemplatedControl { var oldValue = args.OldValue.Value; var newValue = args.NewValue.Value; - if (oldValue != newValue) + if (Math.Abs(oldValue - newValue) > Tolerance) { RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower)); } diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index 7d2a48b..a5e282d 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -22,7 +22,9 @@ public class RangeTrack: Control 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); @@ -165,7 +167,7 @@ public class RangeTrack: Control { var oldValue = args.OldValue.Value; var newValue = args.NewValue.Value; - if (oldValue != newValue) + if (Math.Abs(oldValue - newValue) > Tolerance) { RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower)); } From 2563b96017319b2b783d0acdb56eefec7af9e37a Mon Sep 17 00:00:00 2001 From: Zhang Dian <54255897+zdpcdt@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:09:07 +0800 Subject: [PATCH 20/20] fix: fix vertical direction issue. --- src/Ursa/Controls/RangeSlider/RangeTrack.cs | 60 ++++++++++----------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Ursa/Controls/RangeSlider/RangeTrack.cs b/src/Ursa/Controls/RangeSlider/RangeTrack.cs index a5e282d..a6995dd 100644 --- a/src/Ursa/Controls/RangeSlider/RangeTrack.cs +++ b/src/Ursa/Controls/RangeSlider/RangeTrack.cs @@ -305,26 +305,6 @@ public class RangeTrack: Control CoerceLength(ref lowerThumbLength, finalSize.Height); CoerceLength(ref upperThumbLength, finalSize.Height); if (IsDirectionReversed) - { - 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 { offset = offset.WithY(lowerThumbLength * 0.5); pieceSize = pieceSize.WithHeight(lowerButtonLength); @@ -344,6 +324,26 @@ public class RangeTrack: Control 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 { @@ -485,22 +485,22 @@ public class RangeTrack: Control { if (IsDirectionReversed) { - 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; - } - else - { - double trackStart = LowerThumb?.Bounds.Height/2 ?? 0; + 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