From 13e30799a704dbe3da6a73d1951ab9d1db2ae3c8 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 17 Jan 2024 00:36:55 +0800 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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 From 04995a2df419dd087d4626ebc83ff1e8bc332f29 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 25 Jan 2024 18:01:18 +0800 Subject: [PATCH 21/28] feat: make bindingmode twoway. --- src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs index f51e43a..6166268 100644 --- a/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs +++ b/src/Ursa/Controls/NumericUpDown/NumericUpDownBase.cs @@ -5,6 +5,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Data.Converters; using Avalonia.Input; using Avalonia.Interactivity; @@ -288,7 +289,7 @@ public abstract class NumericUpDown : TemplatedControl public abstract class NumericUpDownBase: NumericUpDown where T: struct, IComparable { public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T?>( - nameof(Value)); + nameof(Value), defaultBindingMode: BindingMode.TwoWay); public T? Value { @@ -297,7 +298,7 @@ public abstract class NumericUpDownBase: NumericUpDown where T: struct, IComp } public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register, T>( - nameof(Maximum), coerce: CoerceMaximum); + nameof(Maximum), defaultBindingMode:BindingMode.TwoWay, coerce: CoerceMaximum); public T Maximum { @@ -306,7 +307,7 @@ public abstract class NumericUpDownBase: NumericUpDown where T: struct, IComp } public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register, T>( - nameof(Minimum), coerce: CoerceMinimum); + nameof(Minimum), defaultBindingMode:BindingMode.TwoWay, coerce: CoerceMinimum); public T Minimum { From d1bb258b280c3f247d9809bc6ad91f75d971c3de Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 25 Jan 2024 18:54:24 +0800 Subject: [PATCH 22/28] feat: initialize and setup demo. --- demo/Ursa.Demo/Models/MenuKeys.cs | 1 + demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml | 19 ++++++ .../Ursa.Demo/Pages/EnumSelectorDemo.axaml.cs | 13 ++++ .../ViewModels/EnumSelectorDemoViewModel.cs | 55 +++++++++++++++ .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 1 + .../Controls/EnumSelector.axaml | 13 ++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + .../Controls/EnumSelector/EnumSelector.cs | 67 +++++++++++++++++++ 9 files changed, 171 insertions(+) create mode 100644 demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/EnumSelector.axaml create mode 100644 src/Ursa/Controls/EnumSelector/EnumSelector.cs diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index 5d9000b..b0eaf2f 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -8,6 +8,7 @@ public static class MenuKeys public const string MenuKeyButtonGroup = "ButtonGroup"; public const string MenuKeyDivider = "Divider"; public const string MenuKeyDualBadge = "DualBadge"; + public const string MenuKeyEnumSelector = "EnumSelector"; public const string MenuKeyImageViewer = "ImageViewer"; public const string MenuKeyIpBox = "IPv4Box"; public const string MenuKeyIconButton = "IconButton"; diff --git a/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml new file mode 100644 index 0000000..2105b49 --- /dev/null +++ b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml.cs b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml.cs new file mode 100644 index 0000000..da6250a --- /dev/null +++ b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class EnumSelectorDemo : UserControl +{ + public EnumSelectorDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs new file mode 100644 index 0000000..20d3297 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class EnumSelectorDemoViewModel: ObservableObject +{ + public ObservableCollection Types { get; set; } + + private Type? _selectedType; + public Type? SelectedType + { + get => _selectedType; + set + { + SetProperty(ref _selectedType, value); + Value = null; + } + } + + private object? _value; + public object? Value + { + get => _value; + set => SetProperty(ref _value, value); + } + + public EnumSelectorDemoViewModel() + { + Types = new ObservableCollection() + { + typeof(HorizontalAlignment), + typeof(VerticalAlignment), + typeof(Orientation), + typeof(Dock), + typeof(GridResizeDirection), + typeof(DayOfWeek), + typeof(FillMode), + typeof(IterationType), + typeof(BindingMode), + typeof(BindingPriority), + typeof(StandardCursorType), + typeof(Key), + typeof(KeyModifiers), + typeof(RoutingStrategies), + }; + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index fbcee7c..8ebf182 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -30,6 +30,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyButtonGroup => new ButtonGroupDemoViewModel(), MenuKeys.MenuKeyDivider => new DividerDemoViewModel(), MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(), + MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(), MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(), MenuKeys.MenuKeyIconButton => new IconButtonDemoViewModel(), MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index a938dc4..d3335cb 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -17,6 +17,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "ButtonGroup", Key = MenuKeys.MenuKeyButtonGroup }, new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider }, new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge }, + new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector }, new() { MenuHeader = "IconButton", Key = MenuKeys.MenuKeyIconButton }, new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer }, new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox }, diff --git a/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml new file mode 100644 index 0000000..064d1a7 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 3417acc..9f871de 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -6,6 +6,7 @@ + diff --git a/src/Ursa/Controls/EnumSelector/EnumSelector.cs b/src/Ursa/Controls/EnumSelector/EnumSelector.cs new file mode 100644 index 0000000..4acdfcb --- /dev/null +++ b/src/Ursa/Controls/EnumSelector/EnumSelector.cs @@ -0,0 +1,67 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; + +namespace Ursa.Controls; + +public class EnumSelector: TemplatedControl +{ + public static readonly StyledProperty EnumTypeProperty = AvaloniaProperty.Register( + nameof(EnumType), validate: OnTypeValidate); + + public Type? EnumType + { + get => GetValue(EnumTypeProperty); + set => SetValue(EnumTypeProperty, value); + } + + private static bool OnTypeValidate(Type? arg) + { + if (arg is null) return true; + return arg.IsEnum; + } + + public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register( + nameof(Value), defaultBindingMode: BindingMode.TwoWay); + + public object? Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + public static readonly DirectProperty?> ValuesProperty = AvaloniaProperty.RegisterDirect?>( + nameof(Values), o => o.Values); + + private IList? _values; + internal IList? Values + { + get => _values; + private set => SetAndRaise(ValuesProperty, ref _values, value); + } + + + static EnumSelector() + { + EnumTypeProperty.Changed.AddClassHandler((o, e) => o.OnTypeChanged(e)); + } + + private void OnTypeChanged(AvaloniaPropertyChangedEventArgs args) + { + var newType = args.GetNewValue(); + if (newType is null || !newType.IsEnum) + { + return; + } + + var values = Enum.GetValues(newType); + List list = new(); + foreach (var value in values) + { + if (value.GetType() == newType) + list.Add(value); + } + Values = list; + } +} \ No newline at end of file From 2855f491654df92557fdeefeda1f65477667b638 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 25 Jan 2024 20:07:51 +0800 Subject: [PATCH 23/28] feat: add value validation. --- demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml | 43 ++++--- .../ViewModels/EnumSelectorDemoViewModel.cs | 12 ++ .../Controls/EnumSelector.axaml | 21 +++- .../Controls/EnumSelector/EnumSelector.cs | 111 ++++++++++++++++-- 4 files changed, 160 insertions(+), 27 deletions(-) diff --git a/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml index 2105b49..b35256d 100644 --- a/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml +++ b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml @@ -1,19 +1,30 @@ - + - - - - - + + + + + + diff --git a/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs index 20d3297..06be6d4 100644 --- a/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs @@ -1,5 +1,7 @@ using System; using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows.Input; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Data; @@ -7,6 +9,7 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; namespace Ursa.Demo.ViewModels; @@ -50,6 +53,15 @@ public class EnumSelectorDemoViewModel: ObservableObject typeof(Key), typeof(KeyModifiers), typeof(RoutingStrategies), + typeof(CustomEnum), }; } +} + +public enum CustomEnum +{ + [Description("是")] + Yes, + [Description("否")] + No, } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml index 064d1a7..b468e53 100644 --- a/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml +++ b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml @@ -4,10 +4,29 @@ xmlns:u="https://irihi.tech/ursa"> + - + + + diff --git a/src/Ursa/Controls/EnumSelector/EnumSelector.cs b/src/Ursa/Controls/EnumSelector/EnumSelector.cs index 4acdfcb..821e6fe 100644 --- a/src/Ursa/Controls/EnumSelector/EnumSelector.cs +++ b/src/Ursa/Controls/EnumSelector/EnumSelector.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -5,6 +6,12 @@ using Avalonia.Data; namespace Ursa.Controls; +public class EnumItemTuple +{ + public string? DisplayName { get; set; } + public object? Value { get; set; } +} + public class EnumSelector: TemplatedControl { public static readonly StyledProperty EnumTypeProperty = AvaloniaProperty.Register( @@ -23,45 +30,129 @@ public class EnumSelector: TemplatedControl } public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register( - nameof(Value), defaultBindingMode: BindingMode.TwoWay); + nameof(Value), defaultBindingMode: BindingMode.TwoWay, coerce:OnValueCoerce); + + private static object? OnValueCoerce(AvaloniaObject o, object? value) + { + if (o is not EnumSelector selector) return null; + if (value is null) return null; + if (value.GetType() != selector.EnumType) return null; + var first = selector.Values.FirstOrDefault(a => Equals(a.Value, value)); + if (first is null) return null; + return value; + } + public object? Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); } + + private EnumItemTuple? _selectedValue; + + public static readonly DirectProperty SelectedValueProperty = AvaloniaProperty.RegisterDirect( + nameof(SelectedValue), o => o.SelectedValue, (o, v) => o.SelectedValue = v); + + public EnumItemTuple? SelectedValue + { + get => _selectedValue; + private set => SetAndRaise(SelectedValueProperty, ref _selectedValue, value); + } - public static readonly DirectProperty?> ValuesProperty = AvaloniaProperty.RegisterDirect?>( + public static readonly DirectProperty?> ValuesProperty = AvaloniaProperty.RegisterDirect?>( nameof(Values), o => o.Values); - private IList? _values; - internal IList? Values + private IList? _values; + internal IList? Values { get => _values; private set => SetAndRaise(ValuesProperty, ref _values, value); } - + + public static readonly StyledProperty DisplayDescriptionProperty = AvaloniaProperty.Register( + nameof(DisplayDescription)); + + public bool DisplayDescription + { + get => GetValue(DisplayDescriptionProperty); + set => SetValue(DisplayDescriptionProperty, value); + } static EnumSelector() { EnumTypeProperty.Changed.AddClassHandler((o, e) => o.OnTypeChanged(e)); + SelectedValueProperty.Changed.AddClassHandler((o, e) => o.OnSelectedValueChanged(e)); + ValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e)); } + private void OnValueChanged(AvaloniaPropertyChangedEventArgs args) + { + if (_updateFromComboBox) return; + var newValue = args.NewValue.Value; + if (newValue is null) + { + SetCurrentValue(SelectedValueProperty, null); + } + else + { + if (newValue.GetType() != EnumType) + { + SetCurrentValue(SelectedValueProperty, null); + } + var tuple = Values?.FirstOrDefault(x => Equals(x.Value, newValue)); + SetCurrentValue(SelectedValueProperty, tuple); + } + } + + private bool _updateFromComboBox; + + private void OnSelectedValueChanged(AvaloniaPropertyChangedEventArgs args) + { + _updateFromComboBox = true; + var newValue = args.NewValue.Value; + SetCurrentValue(ValueProperty, newValue?.Value); + _updateFromComboBox = false; + } + + + private void OnTypeChanged(AvaloniaPropertyChangedEventArgs args) { + Values?.Clear(); var newType = args.GetNewValue(); if (newType is null || !newType.IsEnum) { return; } + Values = GenerateItemTuple(); + } - var values = Enum.GetValues(newType); - List list = new(); + private List GenerateItemTuple() + { + if (EnumType is null) return new List(); + var values = Enum.GetValues(EnumType); + List list = new(); + var fields = EnumType.GetFields(); foreach (var value in values) { - if (value.GetType() == newType) - list.Add(value); + if (value.GetType() == EnumType) + { + var displayName = value.ToString(); + var field = EnumType.GetField(displayName); + var description = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault(); + if (description is not null) + { + displayName = ((DescriptionAttribute) description).Description; + } + list.Add(new EnumItemTuple() + { + DisplayName = displayName, + Value = value + }); + } } - Values = list; + + return list; } } \ No newline at end of file From e8da184f6dfd2ae70de5a7076131a9eaa905b78b Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Jan 2024 17:53:03 +0800 Subject: [PATCH 24/28] feat: add default width. --- src/Ursa.Themes.Semi/Controls/EnumSelector.axaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml index b468e53..841aea3 100644 --- a/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml +++ b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml @@ -4,6 +4,7 @@ xmlns:u="https://irihi.tech/ursa"> + From 7f0c858d8111dd9c824143201fce4db416326080 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Jan 2024 19:20:22 +0800 Subject: [PATCH 25/28] feat: make button group functional. Update Demo. --- demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml | 20 +++++-- .../ViewModels/ButtonGroupDemoViewModel.cs | 28 ++++++++- .../Controls/ButtonGroup.axaml | 1 + src/Ursa/Controls/ButtonGroup.cs | 59 +++++++++++++++++++ 4 files changed, 101 insertions(+), 7 deletions(-) diff --git a/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml b/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml index 30c0a03..3026968 100644 --- a/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml +++ b/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml @@ -13,10 +13,20 @@ mc:Ignorable="d"> - - - - - + + + + + + + + + + diff --git a/demo/Ursa.Demo/ViewModels/ButtonGroupDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ButtonGroupDemoViewModel.cs index 88c5299..02f6ea1 100644 --- a/demo/Ursa.Demo/ViewModels/ButtonGroupDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/ButtonGroupDemoViewModel.cs @@ -1,11 +1,35 @@ using System.Collections.ObjectModel; +using System.Threading.Tasks; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using Ursa.Controls; namespace Ursa.Demo.ViewModels; public class ButtonGroupDemoViewModel: ViewModelBase { - public ObservableCollection Items { get; set; } = new () + public ObservableCollection Items { get; set; } = new () { - "Ding", "Otter", "Husky", "Mr. 17", "Cass" + new ButtonItem(){Name = "Ding" }, + new ButtonItem(){Name = "Otter" }, + new ButtonItem(){Name = "Husky" }, + new ButtonItem(){Name = "Mr. 17" }, + new ButtonItem(){Name = "Cass" }, }; +} + +public class ButtonItem +{ + public string? Name { get; set; } + public ICommand InvokeCommand { get; set; } + + public ButtonItem() + { + InvokeCommand = new AsyncRelayCommand(Invoke); + } + + private async Task Invoke() + { + await MessageBox.ShowAsync("Hello " + Name); + } } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/ButtonGroup.axaml b/src/Ursa.Themes.Semi/Controls/ButtonGroup.axaml index 15a70c1..06136d4 100644 --- a/src/Ursa.Themes.Semi/Controls/ButtonGroup.axaml +++ b/src/Ursa.Themes.Semi/Controls/ButtonGroup.axaml @@ -63,6 +63,7 @@ HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Content="{TemplateBinding Content}" + ContentTemplate="{TemplateBinding ContentTemplate}" FontWeight="{TemplateBinding FontWeight}" Foreground="{TemplateBinding Foreground}" /> diff --git a/src/Ursa/Controls/ButtonGroup.cs b/src/Ursa/Controls/ButtonGroup.cs index 773a1b4..92f997c 100644 --- a/src/Ursa/Controls/ButtonGroup.cs +++ b/src/Ursa/Controls/ButtonGroup.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Data; using Avalonia.Media; using Avalonia.Metadata; @@ -10,6 +11,40 @@ namespace Ursa.Controls; public class ButtonGroup: ItemsControl { + public static readonly StyledProperty CommandBindingProperty = AvaloniaProperty.Register( + nameof(CommandBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? CommandBinding + { + get => GetValue(CommandBindingProperty); + set => SetValue(CommandBindingProperty, value); + } + + public static readonly StyledProperty CommandParameterBindingProperty = AvaloniaProperty.Register( + nameof(CommandParameterBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? CommandParameterBinding + { + get => GetValue(CommandParameterBindingProperty); + set => SetValue(CommandParameterBindingProperty, value); + } + + public static readonly StyledProperty ContentBindingProperty = AvaloniaProperty.Register( + nameof(ContentBinding)); + + [AssignBinding] + [InheritDataTypeFromItems(nameof(ItemsSource))] + public IBinding? ContentBinding + { + get => GetValue(ContentBindingProperty); + set => SetValue(ContentBindingProperty, value); + } + + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { recycleKey = null; @@ -20,4 +55,28 @@ public class ButtonGroup: ItemsControl { return new Button(); } + + protected override void PrepareContainerForItemOverride(Control container, object? item, int index) + { + base.PrepareContainerForItemOverride(container, item, index); + if(container is Button button) + { + if ( CommandBinding is not null) + { + button[!Button.CommandProperty] = CommandBinding; + } + if ( CommandParameterBinding is not null) + { + button[!Button.CommandParameterProperty] = CommandParameterBinding; + } + if ( ContentBinding is not null) + { + button[!Button.ContentProperty] = ContentBinding; + } + if (ItemTemplate is not null) + { + button.ContentTemplate = ItemTemplate; + } + } + } } \ No newline at end of file From 14b6a0fb4bd28d18ee969dbdd90c78e801243469 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Jan 2024 19:23:05 +0800 Subject: [PATCH 26/28] feat: mark updated. --- demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index d3335cb..95ac6f9 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -14,7 +14,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Controls", IsSeparator = true }, new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge }, new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner }, - new() { MenuHeader = "ButtonGroup", Key = MenuKeys.MenuKeyButtonGroup }, + new() { MenuHeader = "ButtonGroup", Key = MenuKeys.MenuKeyButtonGroup, Status = "Updated"}, new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider }, new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge }, new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector }, From ccc883d5f80702697d9d43d42996de79cb7b954b Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 27 Jan 2024 03:47:54 +0800 Subject: [PATCH 27/28] feat: add two tone icon. --- demo/Ursa.Demo/Models/MenuKeys.cs | 1 + .../Ursa.Demo/Pages/TwoTonePathIconDemo.axaml | 18 ++++ .../Pages/TwoTonePathIconDemo.axaml.cs | 13 +++ .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 1 + .../TwoTonePathIconDemoViewModel.cs | 8 ++ .../Controls/TwoTonePathIcon.axaml | 38 ++++++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa/Controls/Icons/TwoTonePathIcon.cs | 92 +++++++++++++++++++ 9 files changed, 173 insertions(+) create mode 100644 demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/TwoTonePathIconDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/TwoTonePathIcon.axaml create mode 100644 src/Ursa/Controls/Icons/TwoTonePathIcon.cs diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index b0eaf2f..17714e1 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -21,5 +21,6 @@ public static class MenuKeys public const string MenuKeyRangeSlider = "RangeSlider"; public const string MenuKeyTagInput = "TagInput"; public const string MenuKeyTimeline = "Timeline"; + public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; } \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml b/demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml new file mode 100644 index 0000000..ae22c4b --- /dev/null +++ b/demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml.cs b/demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml.cs new file mode 100644 index 0000000..72e0a25 --- /dev/null +++ b/demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class TwoTonePathIconDemo : UserControl +{ + public TwoTonePathIconDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 8ebf182..be51f3b 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -43,6 +43,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(), MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(), MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(), + MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), }; } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index 95ac6f9..fa35634 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -30,6 +30,7 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"}, new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput }, new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" }, + new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon, Status = "New"}, }; } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/TwoTonePathIconDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/TwoTonePathIconDemoViewModel.cs new file mode 100644 index 0000000..d2112a1 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/TwoTonePathIconDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class TwoTonePathIconDemoViewModel:ObservableObject +{ + +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/TwoTonePathIcon.axaml b/src/Ursa.Themes.Semi/Controls/TwoTonePathIcon.axaml new file mode 100644 index 0000000..1fe4731 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/TwoTonePathIcon.axaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 9f871de..fc8eba7 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -19,5 +19,6 @@ + diff --git a/src/Ursa/Controls/Icons/TwoTonePathIcon.cs b/src/Ursa/Controls/Icons/TwoTonePathIcon.cs new file mode 100644 index 0000000..d44ad7c --- /dev/null +++ b/src/Ursa/Controls/Icons/TwoTonePathIcon.cs @@ -0,0 +1,92 @@ +using System.ComponentModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Media; + +namespace Ursa.Controls; + +[PseudoClasses(PC_Active)] +public class TwoTonePathIcon: TemplatedControl +{ + public const string PC_Active = ":active"; + + public static readonly StyledProperty StrokeBrushProperty = AvaloniaProperty.Register( + nameof(StrokeBrush)); + + public IBrush? StrokeBrush + { + get => GetValue(StrokeBrushProperty); + set => SetValue(StrokeBrushProperty, value); + } + + public static readonly StyledProperty DataProperty = AvaloniaProperty.Register( + nameof(Data)); + + public Geometry Data + { + get => GetValue(DataProperty); + set => SetValue(DataProperty, value); + } + + public static readonly StyledProperty IsActiveProperty = AvaloniaProperty.Register( + nameof(IsActive), defaultBindingMode: BindingMode.TwoWay); + + public bool IsActive + { + get => GetValue(IsActiveProperty); + set => SetValue(IsActiveProperty, value); + } + + public static readonly StyledProperty ActiveForegroundProperty = AvaloniaProperty.Register( + nameof(ActiveForeground)); + + public IBrush? ActiveForeground + { + get => GetValue(ActiveForegroundProperty); + set => SetValue(ActiveForegroundProperty, value); + } + + public static readonly StyledProperty ActiveStrokeBrushProperty = AvaloniaProperty.Register( + nameof(ActiveStrokeBrush)); + + public IBrush? ActiveStrokeBrush + { + get => GetValue(ActiveStrokeBrushProperty); + set => SetValue(ActiveStrokeBrushProperty, value); + } + + public static readonly StyledProperty StrokeThicknessProperty = + AvaloniaProperty.Register( + nameof(StrokeThickness)); + public double StrokeThickness + { + get => GetValue(StrokeThicknessProperty); + set => SetValue(StrokeThicknessProperty, value); + } + + static TwoTonePathIcon() + { + AffectsRender( + DataProperty, + StrokeBrushProperty, + ForegroundProperty, + ActiveForegroundProperty, + ActiveStrokeBrushProperty); + IsActiveProperty.Changed.AddClassHandler((o, e) => o.OnIsActiveChanged(e)); + } + + private void OnIsActiveChanged(AvaloniaPropertyChangedEventArgs args) + { + var newValue = args.NewValue.Value; + PseudoClasses.Set(PC_Active, newValue); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + PseudoClasses.Set(PC_Active, IsActive); + } +} \ No newline at end of file From 25c36c941c1ddefb4ef30254ee5af2c66b475d6f Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 27 Jan 2024 14:03:39 +0800 Subject: [PATCH 28/28] feat: improve button group demo. --- demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml b/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml index 3026968..4081616 100644 --- a/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml +++ b/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml @@ -18,7 +18,10 @@ ItemsSource="{Binding Items}" > - + + + +