diff --git a/demo/Ursa.Demo/Models/MenuKeys.cs b/demo/Ursa.Demo/Models/MenuKeys.cs index 64c2e37..2b30912 100644 --- a/demo/Ursa.Demo/Models/MenuKeys.cs +++ b/demo/Ursa.Demo/Models/MenuKeys.cs @@ -9,6 +9,7 @@ public static class MenuKeys public const string MenuKeyDialog = "Dialog"; 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"; @@ -18,7 +19,9 @@ 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"; + public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; } \ No newline at end of file diff --git a/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml b/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml index 30c0a03..4081616 100644 --- a/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml +++ b/demo/Ursa.Demo/Pages/ButtonGroupDemo.axaml @@ -13,10 +13,23 @@ mc:Ignorable="d"> - - - - - + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml new file mode 100644 index 0000000..b35256d --- /dev/null +++ b/demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml @@ -0,0 +1,30 @@ + + + + + + + + + + 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/Pages/RangeSliderDemo.axaml b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml new file mode 100644 index 0000000..7aa427b --- /dev/null +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + diff --git a/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs new file mode 100644 index 0000000..fe3181d --- /dev/null +++ b/demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs @@ -0,0 +1,15 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Ursa.Demo.ViewModels; + +namespace Ursa.Demo.Pages; + +public partial class RangeSliderDemo : UserControl +{ + public RangeSliderDemo() + { + InitializeComponent(); + this.DataContext = new RangeSliderDemoViewModel(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/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/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/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs new file mode 100644 index 0000000..06be6d4 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows.Input; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +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), + typeof(CustomEnum), + }; + } +} + +public enum CustomEnum +{ + [Description("是")] + Yes, + [Description("否")] + No, +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index a4a24ab..50ab100 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -31,6 +31,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyDialog => new DialogDemoViewModel(), MenuKeys.MenuKeyDivider => new DividerDemoViewModel(), MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(), + MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(), MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(), MenuKeys.MenuKeyIconButton => new IconButtonDemoViewModel(), MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(), @@ -40,8 +41,10 @@ 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(), + MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), }; } } \ No newline at end of file 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 0195ee8..3114d4d 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -14,21 +14,24 @@ 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 = "Dialog", Key = MenuKeys.MenuKeyDialog }, + 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 }, new() { MenuHeader = "IconButton", Key = MenuKeys.MenuKeyIconButton }, new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer }, new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox }, new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput }, new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading }, - new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox }, - new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation }, - new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown }, + new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox, Status = "New" }, + new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation, Status = "WIP" }, + new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" }, new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination }, + new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"}, new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput }, - new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline }, + new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" }, + new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon, Status = "New"}, }; } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs new file mode 100644 index 0000000..9e8fd67 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using Avalonia.Layout; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public partial class RangeSliderDemoViewModel: ObservableObject +{ + public ObservableCollection Orientations { get; set; } = new ObservableCollection() + { + Orientation.Horizontal, + Orientation.Vertical + }; + + [ObservableProperty] private Orientation _orientation; +} \ No newline at end of file diff --git a/demo/Ursa.Demo/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/demo/Ursa.Demo/Views/MainView.axaml b/demo/Ursa.Demo/Views/MainView.axaml index 2a5bce2..e038f33 100644 --- a/demo/Ursa.Demo/Views/MainView.axaml +++ b/demo/Ursa.Demo/Views/MainView.axaml @@ -43,8 +43,21 @@ + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml new file mode 100644 index 0000000..841aea3 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/EnumSelector.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml new file mode 100644 index 0000000..7d66251 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/RangeSlider.axaml @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 03b88dc..f21a847 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -8,6 +8,7 @@ + @@ -17,7 +18,9 @@ + + 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 diff --git a/src/Ursa/Controls/EnumSelector/EnumSelector.cs b/src/Ursa/Controls/EnumSelector/EnumSelector.cs new file mode 100644 index 0000000..821e6fe --- /dev/null +++ b/src/Ursa/Controls/EnumSelector/EnumSelector.cs @@ -0,0 +1,158 @@ +using System.ComponentModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +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( + 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, 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?>( + nameof(Values), o => o.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(); + } + + 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() == 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 + }); + } + } + + return list; + } +} \ No newline at end of file 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 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 { diff --git a/src/Ursa/Controls/RangeSlider/RangeSlider.cs b/src/Ursa/Controls/RangeSlider/RangeSlider.cs new file mode 100644 index 0000000..92f8298 --- /dev/null +++ b/src/Ursa/Controls/RangeSlider/RangeSlider.cs @@ -0,0 +1,304 @@ +using System.Runtime.CompilerServices; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Utilities; + +namespace Ursa.Controls; + +[TemplatePart(PART_Track, typeof(RangeTrack))] +[PseudoClasses(PC_Horizontal, PC_Vertical, PC_Pressed)] +public class RangeSlider: TemplatedControl +{ + public const string PART_Track = "PART_Track"; + private const string PC_Horizontal= ":horizontal"; + private const string PC_Vertical = ":vertical"; + private const string PC_Pressed = ":pressed"; + + private RangeTrack? _track; + private bool _isDragging; + private IDisposable? _pointerPressedDisposable; + private IDisposable? _pointerMoveDisposable; + private IDisposable? _pointerReleasedDisposable; + + private const double Tolerance = 0.0001; + + public static readonly StyledProperty MinimumProperty = RangeTrack.MinimumProperty.AddOwner(); + public double Minimum + { + get => GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); + } + + public static readonly StyledProperty MaximumProperty = RangeTrack.MaximumProperty.AddOwner(); + public double Maximum + { + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + + public static readonly StyledProperty LowerValueProperty = RangeTrack.LowerValueProperty.AddOwner(); + public double LowerValue + { + get => GetValue(LowerValueProperty); + set => SetValue(LowerValueProperty, value); + } + + public static readonly StyledProperty UpperValueProperty = RangeTrack.UpperValueProperty.AddOwner(); + public double UpperValue + { + get => GetValue(UpperValueProperty); + set => SetValue(UpperValueProperty, value); + } + + public static readonly StyledProperty TrackWidthProperty = AvaloniaProperty.Register( + nameof(TrackWidth)); + + public double TrackWidth + { + get => GetValue(TrackWidthProperty); + set => SetValue(TrackWidthProperty, value); + } + + public static readonly StyledProperty OrientationProperty = RangeTrack.OrientationProperty.AddOwner(); + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + public static readonly StyledProperty IsDirectionReversedProperty = + RangeTrack.IsDirectionReversedProperty.AddOwner(); + + public bool IsDirectionReversed + { + get => GetValue(IsDirectionReversedProperty); + set => SetValue(IsDirectionReversedProperty, value); + } + + public static readonly StyledProperty TickFrequencyProperty = AvaloniaProperty.Register( + nameof(TickFrequency)); + + public double TickFrequency + { + get => GetValue(TickFrequencyProperty); + set => SetValue(TickFrequencyProperty, value); + } + + public static readonly StyledProperty?> TicksProperty = + TickBar.TicksProperty.AddOwner(); + + public AvaloniaList? Ticks + { + get => GetValue(TicksProperty); + set => SetValue(TicksProperty, value); + } + + public static readonly StyledProperty TickPlacementProperty = + Slider.TickPlacementProperty.AddOwner(); + + public TickPlacement TickPlacement + { + get => GetValue(TickPlacementProperty); + set => SetValue(TickPlacementProperty, value); + } + + public static readonly StyledProperty IsSnapToTickProperty = AvaloniaProperty.Register( + nameof(IsSnapToTick)); + + public bool IsSnapToTick + { + get => GetValue(IsSnapToTickProperty); + set => SetValue(IsSnapToTickProperty, value); + } + + public static readonly RoutedEvent ValueChangedEvent = + RoutedEvent.Register(nameof(ValueChanged), RoutingStrategies.Bubble); + + public event EventHandler ValueChanged + { + add => AddHandler(ValueChangedEvent, value); + remove => RemoveHandler(ValueChangedEvent, value); + } + + static RangeSlider() + { + PressedMixin.Attach(); + FocusableProperty.OverrideDefaultValue(true); + IsHitTestVisibleProperty.OverrideDefaultValue(true); + OrientationProperty.OverrideDefaultValue(Orientation.Horizontal); + OrientationProperty.Changed.AddClassHandler((o,e)=>o.OnOrientationChanged(e)); + MinimumProperty.OverrideDefaultValue(0); + MaximumProperty.OverrideDefaultValue(100); + LowerValueProperty.OverrideDefaultValue(0); + UpperValueProperty.OverrideDefaultValue(100); + LowerValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, true)); + UpperValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, false)); + } + + private void OnValueChanged(AvaloniaPropertyChangedEventArgs args, bool isLower) + { + var oldValue = args.OldValue.Value; + var newValue = args.NewValue.Value; + if (Math.Abs(oldValue - newValue) > Tolerance) + { + RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower)); + } + } + + public RangeSlider() + { + UpdatePseudoClasses(Orientation); + } + + private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs args) + { + var value = args.NewValue.Value; + UpdatePseudoClasses(value); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _pointerMoveDisposable?.Dispose(); + _pointerPressedDisposable?.Dispose(); + _pointerReleasedDisposable?.Dispose(); + _track = e.NameScope.Find(PART_Track); + _pointerMoveDisposable = this.AddDisposableHandler(PointerMovedEvent, PointerMove, RoutingStrategies.Tunnel); + _pointerPressedDisposable = this.AddDisposableHandler(PointerPressedEvent, PointerPress, RoutingStrategies.Tunnel); + _pointerReleasedDisposable = this.AddDisposableHandler(PointerReleasedEvent, PointerRelease, RoutingStrategies.Tunnel); + } + + private Thumb? _currentThumb; + + private void PointerPress(object sender, PointerPressedEventArgs e) + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + var point = e.GetCurrentPoint(_track); + _currentThumb = GetThumbByPoint(point); + MoveToPoint(point); + _isDragging = true; + } + } + + private void PointerMove(object sender, PointerEventArgs args) + { + if (!IsEnabled) + { + _isDragging = false; + return; + } + if (_isDragging) + { + MoveToPoint(args.GetCurrentPoint(_track)); + } + } + + private void PointerRelease(object sender, PointerReleasedEventArgs e) + { + _isDragging = false; + _currentThumb = null; + } + + private void MoveToPoint(PointerPoint posOnTrack) + { + if (_track is null) return; + var value = GetValueByPoint(posOnTrack); + var thumb = GetThumbByPoint(posOnTrack); + if (_currentThumb !=null && _currentThumb != thumb) return; + if (thumb is null) return; + if (thumb == _track.LowerThumb) + { + SetCurrentValue(LowerValueProperty, IsSnapToTick ? SnapToTick(value) : value); + } + else + { + SetCurrentValue(UpperValueProperty, IsSnapToTick ? SnapToTick(value) : value); + } + } + + private double SnapToTick(double value) + { + if (IsSnapToTick) + { + var previous = Minimum; + var next = Maximum; + + var ticks = Ticks; + + if (ticks != null && ticks.Count > 0) + { + foreach (var tick in ticks) + { + if (MathUtilities.AreClose(tick, value)) + { + return value; + } + + if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous)) + { + previous = tick; + } + else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next)) + { + next = tick; + } + } + } + else if (MathUtilities.GreaterThan(TickFrequency, 0.0)) + { + previous = Minimum + Math.Round((value - Minimum) / TickFrequency) * TickFrequency; + next = Math.Min(Maximum, previous + TickFrequency); + } + value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous; + } + + return value; + } + + private Thumb? GetThumbByPoint(PointerPoint point) + { + var isHorizontal = Orientation == Orientation.Horizontal; + var lowerThumbPosition = isHorizontal? _track?.LowerThumb?.Bounds.Center.X : _track?.LowerThumb?.Bounds.Center.Y; + var upperThumbPosition = isHorizontal? _track?.UpperThumb?.Bounds.Center.X : _track?.UpperThumb?.Bounds.Center.Y; + var pointerPosition = isHorizontal? point.Position.X : point.Position.Y; + + var lowerDistance = Math.Abs((lowerThumbPosition ?? 0) - pointerPosition); + var upperDistance = Math.Abs((upperThumbPosition ?? 0) - pointerPosition); + + if (lowerDistance +/// 1. Notice that this is not used in ScrollBar, so ViewportSize related feature is not necessary. +/// 2. Maximum, Minimum, MaxValue and MinValue are coerced there. +/// +[PseudoClasses(PC_Horizontal, PC_Vertical)] +public class RangeTrack: Control +{ + public const string PC_Horizontal = ":horizontal"; + public const string PC_Vertical = ":vertical"; + private double _density; + private Vector _lastDrag; + + private const double Tolerance = 0.0001; + + public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register( + nameof(Minimum), coerce: CoerceMinimum, defaultBindingMode:BindingMode.TwoWay); + + public double Minimum + { + get => GetValue(MinimumProperty); + set => SetValue(MinimumProperty, value); + } + + public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register( + nameof(Maximum), coerce: CoerceMaximum, defaultBindingMode: BindingMode.TwoWay); + + public double Maximum + { + get => GetValue(MaximumProperty); + set => SetValue(MaximumProperty, value); + } + + public static readonly StyledProperty LowerValueProperty = AvaloniaProperty.Register( + nameof(LowerValue), coerce: CoerceLowerValue, defaultBindingMode: BindingMode.TwoWay); + + public double LowerValue + { + get => GetValue(LowerValueProperty); + set => SetValue(LowerValueProperty, value); + } + + public static readonly StyledProperty UpperValueProperty = AvaloniaProperty.Register( + nameof(UpperValue), coerce: CoerceUpperValue, defaultBindingMode: BindingMode.TwoWay); + + public double UpperValue + { + get => GetValue(UpperValueProperty); + set => SetValue(UpperValueProperty, value); + } + + public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register( + nameof(Orientation)); + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + public static readonly StyledProperty UpperSectionProperty = AvaloniaProperty.Register( + nameof(UpperSection)); + + public Control? UpperSection + { + get => GetValue(UpperSectionProperty); + set => SetValue(UpperSectionProperty, value); + } + + public static readonly StyledProperty LowerSectionProperty = AvaloniaProperty.Register( + nameof(LowerSection)); + + public Control? LowerSection + { + get => GetValue(LowerSectionProperty); + set => SetValue(LowerSectionProperty, value); + } + + public static readonly StyledProperty InnerSectionProperty = AvaloniaProperty.Register( + nameof(InnerSection)); + + public Control? InnerSection + { + get => GetValue(InnerSectionProperty); + set => SetValue(InnerSectionProperty, value); + } + + public static readonly StyledProperty TrackBackgroundProperty = AvaloniaProperty.Register( + nameof(TrackBackground)); + + public Control? TrackBackground + { + get => GetValue(TrackBackgroundProperty); + set => SetValue(TrackBackgroundProperty, value); + } + + public static readonly StyledProperty UpperThumbProperty = AvaloniaProperty.Register( + nameof(UpperThumb)); + + public Thumb? UpperThumb + { + get => GetValue(UpperThumbProperty); + set => SetValue(UpperThumbProperty, value); + } + + public static readonly StyledProperty LowerThumbProperty = AvaloniaProperty.Register( + nameof(LowerThumb)); + + public Thumb? LowerThumb + { + get => GetValue(LowerThumbProperty); + set => SetValue(LowerThumbProperty, value); + } + + public static readonly StyledProperty IsDirectionReversedProperty = AvaloniaProperty.Register( + nameof(IsDirectionReversed)); + + public bool IsDirectionReversed + { + get => GetValue(IsDirectionReversedProperty); + set => SetValue(IsDirectionReversedProperty, value); + } + + public static readonly RoutedEvent ValueChangedEvent = + RoutedEvent.Register(nameof(ValueChanged), RoutingStrategies.Bubble); + + public event EventHandler ValueChanged + { + add => AddHandler(ValueChangedEvent, value); + remove => RemoveHandler(ValueChangedEvent, value); + } + + static RangeTrack() + { + OrientationProperty.Changed.AddClassHandler((o, e) => o.OnOrientationChanged(e)); + LowerThumbProperty.Changed.AddClassHandler((o, e) => o.OnThumbChanged(e)); + UpperThumbProperty.Changed.AddClassHandler((o, e) => o.OnThumbChanged(e)); + LowerSectionProperty.Changed.AddClassHandler((o, e) => o.OnSectionChanged(e)); + UpperSectionProperty.Changed.AddClassHandler((o, e) => o.OnSectionChanged(e)); + InnerSectionProperty.Changed.AddClassHandler((o, e) => o.OnSectionChanged(e)); + MinimumProperty.Changed.AddClassHandler((o, e) => o.OnMinimumChanged(e)); + MaximumProperty.Changed.AddClassHandler((o, e) => o.OnMaximumChanged(e)); + LowerValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, true)); + UpperValueProperty.Changed.AddClassHandler((o, e) => o.OnValueChanged(e, false)); + AffectsArrange( + MinimumProperty, + MaximumProperty, + LowerValueProperty, + UpperValueProperty, + OrientationProperty, + IsDirectionReversedProperty); + } + + private void OnValueChanged(AvaloniaPropertyChangedEventArgs args, bool isLower) + { + var oldValue = args.OldValue.Value; + var newValue = args.NewValue.Value; + if (Math.Abs(oldValue - newValue) > Tolerance) + { + RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower)); + } + } + + private void OnMinimumChanged(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs) + { + if (IsInitialized) + { + CoerceValue(MaximumProperty); + CoerceValue(LowerValueProperty); + CoerceValue(UpperValueProperty); + } + } + + private void OnMaximumChanged(AvaloniaPropertyChangedEventArgs avaloniaPropertyChangedEventArgs) + { + if (IsInitialized) + { + CoerceValue(LowerValueProperty); + CoerceValue(UpperValueProperty); + } + } + + private void OnSectionChanged(AvaloniaPropertyChangedEventArgs args) + { + var oldSection = args.OldValue.Value; + var newSection = args.NewValue.Value; + if (oldSection is not null) + { + LogicalChildren.Remove(oldSection); + VisualChildren.Remove(oldSection); + } + if (newSection is not null) + { + LogicalChildren.Add(newSection); + VisualChildren.Add(newSection); + } + } + + private void OnThumbChanged(AvaloniaPropertyChangedEventArgs args) + { + var oldThumb = args.OldValue.Value; + var newThumb = args.NewValue.Value; + if(oldThumb is not null) + { + LogicalChildren.Remove(oldThumb); + VisualChildren.Remove(oldThumb); + } + if (newThumb is not null) + { + newThumb.ZIndex = 5; + LogicalChildren.Add(newThumb); + VisualChildren.Add(newThumb); + } + } + + private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs args) + { + Orientation o = args.NewValue.Value; + PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal); + PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical); + } + + private static double CoerceMaximum(AvaloniaObject sender, double value) + { + return ValidateDouble(value) + ? Math.Max(value, sender.GetValue(MinimumProperty)) + : sender.GetValue(MaximumProperty); + } + + private static double CoerceMinimum(AvaloniaObject sender, double value) + { + return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty); + } + + private static double CoerceLowerValue(AvaloniaObject sender, double value) + { + if (!ValidateDouble(value)) return sender.GetValue(LowerValueProperty); + value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty)); + value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(UpperValueProperty)); + return value; + } + + private static double CoerceUpperValue(AvaloniaObject sender, double value) + { + if (!ValidateDouble(value)) return sender.GetValue(UpperValueProperty); + value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty)); + value = MathUtilities.Clamp(value, sender.GetValue(LowerValueProperty), sender.GetValue(MaximumProperty)); + return value; + } + + protected override void OnInitialized() + { + base.OnInitialized(); + CoerceValue(MaximumProperty); + CoerceValue(LowerValueProperty); + CoerceValue(UpperValueProperty); + } + + protected override Size MeasureOverride(Size availableSize) + { + var desiredSize = new Size(); + if (LowerThumb is not null && UpperThumb is not null) + { + LowerThumb.Measure(availableSize); + UpperThumb.Measure(availableSize); + if (Orientation == Orientation.Horizontal) + { + desiredSize = new Size(LowerThumb.DesiredSize.Width + UpperThumb.DesiredSize.Width, + Math.Max(LowerThumb.DesiredSize.Height, UpperThumb.DesiredSize.Height)); + } + else + { + desiredSize = new Size(Math.Max(LowerThumb.DesiredSize.Width, UpperThumb.DesiredSize.Width), + LowerThumb.DesiredSize.Height + UpperThumb.DesiredSize.Height); + } + } + return desiredSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + var vertical = Orientation == Orientation.Vertical; + double lowerButtonLength, innerButtonLength, upperButtonLength, lowerThumbLength, upperThumbLength; + ComputeSliderLengths(finalSize, vertical, out lowerButtonLength, out innerButtonLength, out upperButtonLength, + out lowerThumbLength, out upperThumbLength); + var offset = new Point(); + var pieceSize = finalSize; + if (vertical) + { + CoerceLength(ref lowerButtonLength, finalSize.Height); + CoerceLength(ref innerButtonLength, finalSize.Height); + CoerceLength(ref upperButtonLength, finalSize.Height); + CoerceLength(ref lowerThumbLength, finalSize.Height); + CoerceLength(ref upperThumbLength, finalSize.Height); + if (IsDirectionReversed) + { + offset = offset.WithY(lowerThumbLength * 0.5); + pieceSize = pieceSize.WithHeight(lowerButtonLength); + LowerSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithY(offset.Y + lowerButtonLength); + pieceSize = pieceSize.WithHeight(innerButtonLength); + InnerSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithY(offset.Y + innerButtonLength); + pieceSize = pieceSize.WithHeight(upperButtonLength); + UpperSection?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithY(lowerButtonLength); + pieceSize = pieceSize.WithHeight(lowerThumbLength); + LowerThumb?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithY(lowerButtonLength + innerButtonLength); + pieceSize = pieceSize.WithHeight(upperThumbLength); + UpperThumb?.Arrange(new Rect(offset, pieceSize)); + } + else + { + offset = offset.WithY(upperThumbLength * 0.5); + pieceSize = pieceSize.WithHeight(upperButtonLength); + UpperSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithY(offset.Y + upperButtonLength); + pieceSize = pieceSize.WithHeight(innerButtonLength); + InnerSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithY(offset.Y + innerButtonLength); + pieceSize = pieceSize.WithHeight(lowerButtonLength); + LowerSection?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithY(upperButtonLength); + pieceSize = pieceSize.WithHeight(upperThumbLength); + UpperThumb?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithY(upperButtonLength + innerButtonLength); + pieceSize = pieceSize.WithHeight(lowerThumbLength); + LowerThumb?.Arrange(new Rect(offset, pieceSize)); + } + } + else + { + CoerceLength(ref lowerButtonLength, finalSize.Width); + CoerceLength(ref innerButtonLength, finalSize.Width); + CoerceLength(ref upperButtonLength, finalSize.Width); + CoerceLength(ref lowerThumbLength, finalSize.Width); + CoerceLength(ref upperThumbLength, finalSize.Width); + if (IsDirectionReversed) + { + offset = offset.WithX(upperThumbLength * 0.5); + pieceSize = pieceSize.WithWidth(upperButtonLength); + UpperSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithX(offset.X + upperButtonLength); + pieceSize = pieceSize.WithWidth(innerButtonLength); + InnerSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithX(offset.X + innerButtonLength); + pieceSize = pieceSize.WithWidth(lowerButtonLength); + LowerSection?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithX(upperButtonLength); + pieceSize = pieceSize.WithWidth(upperThumbLength); + UpperThumb?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithX(upperButtonLength+innerButtonLength); + pieceSize = pieceSize.WithWidth(lowerThumbLength); + LowerThumb?.Arrange(new Rect(offset, pieceSize)); + } + else + { + offset = offset.WithX(lowerThumbLength * 0.5); + pieceSize = pieceSize.WithWidth(lowerButtonLength); + LowerSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithX(offset.X + lowerButtonLength); + pieceSize = pieceSize.WithWidth(innerButtonLength); + InnerSection?.Arrange(new Rect(offset, pieceSize)); + offset = offset.WithX(offset.X + innerButtonLength); + pieceSize = pieceSize.WithWidth(upperButtonLength); + UpperSection?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithX(lowerButtonLength); + pieceSize = pieceSize.WithWidth(lowerThumbLength); + LowerThumb?.Arrange(new Rect(offset, pieceSize)); + + offset = offset.WithX(lowerButtonLength + innerButtonLength); + pieceSize = pieceSize.WithWidth(upperThumbLength); + UpperThumb?.Arrange(new Rect(offset, pieceSize)); + + } + } + return finalSize; + } + + private void ComputeSliderLengths( + Size arrangeSize, + bool isVertical, + out double lowerButtonLength, + out double innerButtonLength, + out double upperButtonLength, + out double lowerThumbLength, + out double upperThumbLength) + { + double range = Math.Max(0, Maximum - Minimum); + range += double.Epsilon; + double lowerOffset = Math.Min(range, LowerValue - Minimum); + double upperOffset = Math.Min(range, UpperValue - Minimum); + + double trackLength; + if (isVertical) + { + trackLength = arrangeSize.Height; + lowerThumbLength = LowerThumb?.DesiredSize.Height ?? 0; + upperThumbLength = UpperThumb?.DesiredSize.Height ?? 0; + } + else + { + trackLength = arrangeSize.Width; + lowerThumbLength = LowerThumb?.DesiredSize.Width ?? 0; + upperThumbLength = UpperThumb?.DesiredSize.Width ?? 0; + } + + CoerceLength(ref lowerThumbLength, trackLength); + CoerceLength(ref upperThumbLength, trackLength); + + double remainingLength = trackLength - lowerThumbLength * 0.5 - upperThumbLength * 0.5; + + lowerButtonLength = remainingLength * lowerOffset / range; + upperButtonLength = remainingLength * (range-upperOffset) / range; + innerButtonLength = remainingLength - lowerButtonLength - upperButtonLength; + + _density = range / remainingLength; + } + + private static void CoerceLength(ref double componentLength, double trackLength) + { + if (componentLength < 0) + { + componentLength = 0.0; + } + else if (componentLength > trackLength || double.IsNaN(componentLength)) + { + componentLength = trackLength; + } + } + + private static bool ValidateDouble(double value) + { + return !double.IsInfinity(value) && !double.IsNaN(value); + } + + internal double GetRatioByPoint(double position) + { + bool isHorizontal = Orientation == Orientation.Horizontal; + var range = isHorizontal? + LowerSection?.Bounds.Width + InnerSection?.Bounds.Width + UpperSection?.Bounds.Width ?? double.Epsilon + : LowerSection?.Bounds.Height + InnerSection?.Bounds.Height + UpperSection?.Bounds.Height ?? double.Epsilon; + if (isHorizontal) + { + if (IsDirectionReversed) + { + double trackStart = UpperThumb?.Bounds.Width/2 ?? 0; + double trackEnd = trackStart + range; + if (position < trackStart) return 1.0; + if (position > trackEnd) return 0.0; + double diff = trackEnd - position; + return diff / range; + } + else + { + double trackStart = LowerThumb?.Bounds.Width/2 ?? 0; + double trackEnd = trackStart + range; + if (position < trackStart) return 0.0; + if (position > trackEnd) return 1.0; + double diff = position - trackStart; + return diff / range; + } + } + else + { + if (IsDirectionReversed) + { + double trackStart = LowerThumb?.Bounds.Height / 2 ?? 0; + double trackEnd = trackStart + range; + if (position < trackStart) return 0.0; + if (position > trackEnd) return 1.0; + double diff = position - trackStart; + return diff / range; + } + else + { + double trackStart = UpperThumb?.Bounds.Height / 2 ?? 0; + double trackEnd = trackStart + range; + if (position < trackStart) return 1.0; + if (position > trackEnd) return 0.0; + double diff = trackEnd - position; + return diff / range; + } + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs b/src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs new file mode 100644 index 0000000..2effee1 --- /dev/null +++ b/src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs @@ -0,0 +1,34 @@ +using Avalonia.Interactivity; + +namespace Ursa.Controls; + +public class RangeValueChangedEventArgs: RoutedEventArgs +{ + public double OldValue { get; set; } + public double NewValue { get; set; } + public bool IsLower { get; set; } + + public RangeValueChangedEventArgs( + RoutedEvent routedEvent, + object source, + double oldValue, + double newValue, + bool isLower = true) : base(routedEvent, source) + { + OldValue = oldValue; + NewValue = newValue; + IsLower = isLower; + } + + public RangeValueChangedEventArgs( + RoutedEvent routedEvent, + double oldValue, + double newValue, + bool isLower = true) : base(routedEvent) + { + OldValue = oldValue; + NewValue = newValue; + IsLower = isLower; + } + +} \ No newline at end of file