Merge branch 'main' into dialog
This commit is contained in:
@@ -9,6 +9,7 @@ public static class MenuKeys
|
|||||||
public const string MenuKeyDialog = "Dialog";
|
public const string MenuKeyDialog = "Dialog";
|
||||||
public const string MenuKeyDivider = "Divider";
|
public const string MenuKeyDivider = "Divider";
|
||||||
public const string MenuKeyDualBadge = "DualBadge";
|
public const string MenuKeyDualBadge = "DualBadge";
|
||||||
|
public const string MenuKeyEnumSelector = "EnumSelector";
|
||||||
public const string MenuKeyImageViewer = "ImageViewer";
|
public const string MenuKeyImageViewer = "ImageViewer";
|
||||||
public const string MenuKeyIpBox = "IPv4Box";
|
public const string MenuKeyIpBox = "IPv4Box";
|
||||||
public const string MenuKeyIconButton = "IconButton";
|
public const string MenuKeyIconButton = "IconButton";
|
||||||
@@ -18,7 +19,9 @@ public static class MenuKeys
|
|||||||
public const string MenuKeyNavigation = "Navigation";
|
public const string MenuKeyNavigation = "Navigation";
|
||||||
public const string MenuKeyNumericUpDown = "NumericUpDown";
|
public const string MenuKeyNumericUpDown = "NumericUpDown";
|
||||||
public const string MenuKeyPagination = "Pagination";
|
public const string MenuKeyPagination = "Pagination";
|
||||||
|
public const string MenuKeyRangeSlider = "RangeSlider";
|
||||||
public const string MenuKeyTagInput = "TagInput";
|
public const string MenuKeyTagInput = "TagInput";
|
||||||
public const string MenuKeyTimeline = "Timeline";
|
public const string MenuKeyTimeline = "Timeline";
|
||||||
|
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -13,10 +13,23 @@
|
|||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<StackPanel Margin="20" Spacing="20">
|
<StackPanel Margin="20" Spacing="20">
|
||||||
<u:ButtonGroup Classes="Primary Solid" ItemsSource="{Binding Items}" />
|
<u:ButtonGroup Classes="Primary Solid"
|
||||||
<u:ButtonGroup Classes="Primary" ItemsSource="{Binding Items}" />
|
CommandBinding="{Binding InvokeCommand}"
|
||||||
<u:ButtonGroup Classes="Primary Borderless" ItemsSource="{Binding Items}" />
|
ItemsSource="{Binding Items}" >
|
||||||
<u:ButtonGroup Classes="Primary Large" ItemsSource="{Binding Items}" />
|
<u:ButtonGroup.ItemTemplate>
|
||||||
<u:ButtonGroup Classes="Primary Small" ItemsSource="{Binding Items}" />
|
<DataTemplate x:DataType="vm:ButtonItem">
|
||||||
|
<TextBlock>
|
||||||
|
<Run Text="🐼"></Run>
|
||||||
|
<Run Text="{Binding Name}"></Run>
|
||||||
|
</TextBlock>
|
||||||
|
</DataTemplate>
|
||||||
|
</u:ButtonGroup.ItemTemplate>
|
||||||
|
</u:ButtonGroup>
|
||||||
|
|
||||||
|
<u:ButtonGroup Classes="Primary"
|
||||||
|
ContentBinding="{Binding Name}"
|
||||||
|
CommandBinding="{Binding InvokeCommand}"
|
||||||
|
ItemsSource="{Binding Items}" >
|
||||||
|
</u:ButtonGroup>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
30
demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml
Normal file
30
demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Ursa.Demo.Pages.EnumSelectorDemo"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
|
xmlns:vm="clr-namespace:Ursa.Demo.ViewModels;assembly=Ursa.Demo"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
x:DataType="vm:EnumSelectorDemoViewModel"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<StackPanel>
|
||||||
|
<ToggleSwitch Name="description" Content="Show Description" />
|
||||||
|
<TextBlock Text="Select Type" />
|
||||||
|
<ComboBox
|
||||||
|
Width="200"
|
||||||
|
DisplayMemberBinding="{Binding Name}"
|
||||||
|
ItemsSource="{Binding Types}"
|
||||||
|
SelectedItem="{Binding SelectedType}" />
|
||||||
|
<TextBlock Text="Select Value" />
|
||||||
|
<u:EnumSelector
|
||||||
|
Width="200"
|
||||||
|
DisplayDescription="{Binding #description.IsChecked}"
|
||||||
|
EnumType="{Binding SelectedType}"
|
||||||
|
Value="{Binding Value}" />
|
||||||
|
<TextBlock Text="{Binding Value}" />
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
13
demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/EnumSelectorDemo.axaml.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
52
demo/Ursa.Demo/Pages/RangeSliderDemo.axaml
Normal file
52
demo/Ursa.Demo/Pages/RangeSliderDemo.axaml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Ursa.Demo.Pages.RangeSliderDemo"
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
|
xmlns:vm="using:Ursa.Demo.ViewModels"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800"
|
||||||
|
x:CompileBindings="True"
|
||||||
|
x:DataType="vm:RangeSliderDemoViewModel"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="u|NumericDoubleUpDown">
|
||||||
|
<Setter Property="Width" Value="300" />
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
<StackPanel>
|
||||||
|
<u:RangeSlider
|
||||||
|
IsSnapToTick="True"
|
||||||
|
TickFrequency="5"
|
||||||
|
TickPlacement="TopLeft"
|
||||||
|
Minimum="{Binding #range.Minimum, Mode=TwoWay}"
|
||||||
|
Maximum="{Binding #range.Maximum, Mode=TwoWay}"
|
||||||
|
LowerValue="{Binding #range.LowerValue, Mode=TwoWay}"
|
||||||
|
UpperValue="{Binding #range.UpperValue, Mode=TwoWay}"/>
|
||||||
|
<u:NumericDoubleUpDown InnerLeftContent="Minimum" Value="{Binding #range.Minimum, Mode=TwoWay}" />
|
||||||
|
<u:NumericDoubleUpDown InnerLeftContent="Maximum" Value="{Binding #range.Maximum, Mode=TwoWay}" />
|
||||||
|
<u:NumericDoubleUpDown InnerLeftContent="LowerValue" Value="{Binding #range.LowerValue, Mode=TwoWay}" />
|
||||||
|
<u:NumericDoubleUpDown InnerLeftContent="UpperValue" Value="{Binding #range.UpperValue, Mode=TwoWay}" />
|
||||||
|
<ComboBox ItemsSource="{Binding Orientations}" SelectedItem="{Binding Orientation}" />
|
||||||
|
<u:RangeSlider
|
||||||
|
Name="range"
|
||||||
|
Margin="8"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Orientation="{Binding Orientation}"
|
||||||
|
TickFrequency="5">
|
||||||
|
<u:RangeSlider.Styles>
|
||||||
|
<Style Selector="u|RangeSlider:horizontal">
|
||||||
|
<Setter Property="Width" Value="400" />
|
||||||
|
<Setter Property="Height" Value="40" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="u|RangeSlider:vertical">
|
||||||
|
<Setter Property="Width" Value="40" />
|
||||||
|
<Setter Property="Height" Value="400" />
|
||||||
|
</Style>
|
||||||
|
</u:RangeSlider.Styles>
|
||||||
|
</u:RangeSlider>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
15
demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs
Normal file
15
demo/Ursa.Demo/Pages/RangeSliderDemo.axaml.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
18
demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml
Normal file
18
demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:u="https://irihi.tech/ursa"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Ursa.Demo.Pages.TwoTonePathIconDemo">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Size"></TextBlock>
|
||||||
|
<Slider Name="width" Minimum="0" Maximum="100" Value="20" Width="300"></Slider>
|
||||||
|
<ToggleSwitch Name="active" Content="Active"></ToggleSwitch>
|
||||||
|
<u:TwoTonePathIcon
|
||||||
|
IsActive="{Binding ElementName=active, Path=IsChecked}"
|
||||||
|
Width="{Binding #width.Value}"
|
||||||
|
Height="{Binding #width.Value}"
|
||||||
|
Data="M12 3L2 12H5V20H19V12H22L12 3M13 18H11V16H13V18M13 14H11V8H13V14Z"/>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
13
demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/TwoTonePathIconDemo.axaml.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,35 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Ursa.Controls;
|
||||||
|
|
||||||
namespace Ursa.Demo.ViewModels;
|
namespace Ursa.Demo.ViewModels;
|
||||||
|
|
||||||
public class ButtonGroupDemoViewModel: ViewModelBase
|
public class ButtonGroupDemoViewModel: ViewModelBase
|
||||||
{
|
{
|
||||||
public ObservableCollection<string> Items { get; set; } = new ()
|
public ObservableCollection<ButtonItem> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
67
demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs
Normal file
67
demo/Ursa.Demo/ViewModels/EnumSelectorDemoViewModel.cs
Normal file
@@ -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<Type?> 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<Type?>()
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ public class MainViewViewModel : ViewModelBase
|
|||||||
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
|
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
|
||||||
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
|
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
|
||||||
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
|
MenuKeys.MenuKeyDualBadge => new DualBadgeDemoViewModel(),
|
||||||
|
MenuKeys.MenuKeyEnumSelector => new EnumSelectorDemoViewModel(),
|
||||||
MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(),
|
MenuKeys.MenuKeyImageViewer => new ImageViewerDemoViewModel(),
|
||||||
MenuKeys.MenuKeyIconButton => new IconButtonDemoViewModel(),
|
MenuKeys.MenuKeyIconButton => new IconButtonDemoViewModel(),
|
||||||
MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(),
|
MenuKeys.MenuKeyIpBox => new IPv4BoxDemoViewModel(),
|
||||||
@@ -40,8 +41,10 @@ public class MainViewViewModel : ViewModelBase
|
|||||||
MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(),
|
MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(),
|
||||||
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
|
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
|
||||||
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
|
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
|
||||||
|
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
|
||||||
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
||||||
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
||||||
|
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,19 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
|
|
||||||
namespace Ursa.Demo.ViewModels;
|
namespace Ursa.Demo.ViewModels;
|
||||||
|
|
||||||
|
public enum ControlStatus
|
||||||
|
{
|
||||||
|
New,
|
||||||
|
Beta,
|
||||||
|
Stable,
|
||||||
|
}
|
||||||
|
|
||||||
public class MenuItemViewModel: ViewModelBase
|
public class MenuItemViewModel: ViewModelBase
|
||||||
{
|
{
|
||||||
public string MenuHeader { get; set; }
|
public string MenuHeader { get; set; }
|
||||||
public string MenuIconName { get; set; }
|
public string MenuIconName { get; set; }
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
public bool IsSeparator { get; set; }
|
public bool IsSeparator { get; set; }
|
||||||
public ObservableCollection<MenuItemViewModel> Children { get; set; } = new();
|
public ObservableCollection<MenuItemViewModel> Children { get; set; } = new();
|
||||||
|
|||||||
@@ -14,21 +14,24 @@ public class MenuViewModel: ViewModelBase
|
|||||||
new() { MenuHeader = "Controls", IsSeparator = true },
|
new() { MenuHeader = "Controls", IsSeparator = true },
|
||||||
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
|
new() { MenuHeader = "Badge", Key = MenuKeys.MenuKeyBadge },
|
||||||
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner },
|
new() { MenuHeader = "Banner", Key = MenuKeys.MenuKeyBanner },
|
||||||
new() { MenuHeader = "ButtonGroup", Key = MenuKeys.MenuKeyButtonGroup },
|
|
||||||
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
|
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
|
||||||
|
new() { MenuHeader = "ButtonGroup", Key = MenuKeys.MenuKeyButtonGroup, Status = "Updated"},
|
||||||
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
|
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
|
||||||
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
|
new() { MenuHeader = "DualBadge", Key = MenuKeys.MenuKeyDualBadge },
|
||||||
|
new() { MenuHeader = "Enum Selector", Key = MenuKeys.MenuKeyEnumSelector },
|
||||||
new() { MenuHeader = "IconButton", Key = MenuKeys.MenuKeyIconButton },
|
new() { MenuHeader = "IconButton", Key = MenuKeys.MenuKeyIconButton },
|
||||||
new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer },
|
new() { MenuHeader = "ImageViewer", Key = MenuKeys.MenuKeyImageViewer },
|
||||||
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
||||||
new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput },
|
new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput },
|
||||||
new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading },
|
new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading },
|
||||||
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox },
|
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox, Status = "New" },
|
||||||
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation },
|
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation, Status = "WIP" },
|
||||||
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
|
||||||
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
||||||
|
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"},
|
||||||
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
|
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"},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs
Normal file
16
demo/Ursa.Demo/ViewModels/RangeSliderDemoViewModel.cs
Normal file
@@ -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<Orientation> Orientations { get; set; } = new ObservableCollection<Orientation>()
|
||||||
|
{
|
||||||
|
Orientation.Horizontal,
|
||||||
|
Orientation.Vertical
|
||||||
|
};
|
||||||
|
|
||||||
|
[ObservableProperty] private Orientation _orientation;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Ursa.Demo.ViewModels;
|
||||||
|
|
||||||
|
public class TwoTonePathIconDemoViewModel:ObservableObject
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -43,8 +43,21 @@
|
|||||||
<DataTemplate DataType="vm:MenuItemViewModel">
|
<DataTemplate DataType="vm:MenuItemViewModel">
|
||||||
<u:NavigationMenuItem
|
<u:NavigationMenuItem
|
||||||
Command="{Binding ActivateCommand}"
|
Command="{Binding ActivateCommand}"
|
||||||
Header="{Binding MenuHeader}"
|
Header="{Binding}"
|
||||||
ItemsSource="{Binding Children}">
|
ItemsSource="{Binding Children}">
|
||||||
|
<u:NavigationMenuItem.HeaderTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:MenuItemViewModel">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock HorizontalAlignment="Left" Text="{Binding MenuHeader}" />
|
||||||
|
<u:Badge
|
||||||
|
Margin="8,0,0,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
BadgeContent="{Binding Status}"
|
||||||
|
CornerPosition="TopRight" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</DataTemplate>
|
||||||
|
</u:NavigationMenuItem.HeaderTemplate>
|
||||||
<u:NavigationMenuItem.Icon>
|
<u:NavigationMenuItem.Icon>
|
||||||
<Border
|
<Border
|
||||||
Width="10"
|
Width="10"
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||||
Content="{TemplateBinding Content}"
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
FontWeight="{TemplateBinding FontWeight}"
|
FontWeight="{TemplateBinding FontWeight}"
|
||||||
Foreground="{TemplateBinding Foreground}" />
|
Foreground="{TemplateBinding Foreground}" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
33
src/Ursa.Themes.Semi/Controls/EnumSelector.axaml
Normal file
33
src/Ursa.Themes.Semi/Controls/EnumSelector.axaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<ResourceDictionary
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:u="https://irihi.tech/ursa">
|
||||||
|
<!-- Add Resources Here -->
|
||||||
|
<ControlTheme x:Key="{x:Type u:EnumSelector}" TargetType="u:EnumSelector">
|
||||||
|
<Setter Property="Width" Value="100" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"></Setter>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:EnumSelector">
|
||||||
|
<ComboBox
|
||||||
|
Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Bounds.Width}"
|
||||||
|
Name="PART_ComboBox"
|
||||||
|
ItemsSource="{TemplateBinding Values}"
|
||||||
|
SelectedItem="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedValue, Mode=TwoWay}" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
<Style Selector="^[DisplayDescription=True] /template/ ComboBox">
|
||||||
|
<Setter Property="ItemTemplate">
|
||||||
|
<DataTemplate x:DataType="u:EnumItemTuple">
|
||||||
|
<TextBlock Text="{Binding DisplayName}"></TextBlock>
|
||||||
|
</DataTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^[DisplayDescription=False] /template/ ComboBox">
|
||||||
|
<Setter Property="ItemTemplate">
|
||||||
|
<DataTemplate x:DataType="u:EnumItemTuple">
|
||||||
|
<TextBlock Text="{Binding Value}"></TextBlock>
|
||||||
|
</DataTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
228
src/Ursa.Themes.Semi/Controls/RangeSlider.axaml
Normal file
228
src/Ursa.Themes.Semi/Controls/RangeSlider.axaml
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<ResourceDictionary
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:u="https://irihi.tech/ursa">
|
||||||
|
<!-- Add Resources Here -->
|
||||||
|
<ControlTheme x:Key="{x:Type u:RangeSlider}" TargetType="u:RangeSlider">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SliderTrackBackground}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SliderTrackForeground}" />
|
||||||
|
<Setter Property="TrackWidth" Value="4" />
|
||||||
|
<Style Selector="^:horizontal">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:RangeSlider">
|
||||||
|
<DataValidationErrors>
|
||||||
|
<Grid
|
||||||
|
x:Name="SliderContainer"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
Background="Transparent"
|
||||||
|
RowDefinitions="Auto,Auto,Auto">
|
||||||
|
<Grid.Styles>
|
||||||
|
<Style Selector="TickBar">
|
||||||
|
<Setter Property="ReservedSpace" Value="{Binding #PART_Track.LowerThumb.Bounds}" />
|
||||||
|
</Style>
|
||||||
|
</Grid.Styles>
|
||||||
|
<TickBar
|
||||||
|
Name="TopTickBar"
|
||||||
|
Grid.Row="0"
|
||||||
|
Height="{DynamicResource SliderTickHorizontalHeight}"
|
||||||
|
Margin="0,0,0,4"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Fill="{DynamicResource SliderTickForeground}"
|
||||||
|
IsVisible="False"
|
||||||
|
Maximum="{TemplateBinding u:RangeSlider.Maximum}"
|
||||||
|
Minimum="{TemplateBinding u:RangeSlider.Minimum}"
|
||||||
|
Orientation="{TemplateBinding u:RangeSlider.Orientation}"
|
||||||
|
Placement="Top"
|
||||||
|
TickFrequency="{TemplateBinding u:RangeSlider.TickFrequency}"
|
||||||
|
Ticks="{TemplateBinding Ticks}" />
|
||||||
|
<TickBar
|
||||||
|
Name="BottomTickBar"
|
||||||
|
Grid.Row="2"
|
||||||
|
Height="{DynamicResource SliderTickHorizontalHeight}"
|
||||||
|
Margin="0,4,0,0"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Fill="{DynamicResource SliderTickForeground}"
|
||||||
|
IsVisible="False"
|
||||||
|
Maximum="{TemplateBinding u:RangeSlider.Maximum}"
|
||||||
|
Minimum="{TemplateBinding u:RangeSlider.Minimum}"
|
||||||
|
Orientation="{TemplateBinding u:RangeSlider.Orientation}"
|
||||||
|
Placement="Bottom"
|
||||||
|
TickFrequency="{TemplateBinding u:RangeSlider.TickFrequency}"
|
||||||
|
Ticks="{TemplateBinding Ticks}" />
|
||||||
|
<u:RangeTrack
|
||||||
|
Name="{x:Static u:RangeSlider.PART_Track}"
|
||||||
|
Grid.Row="1"
|
||||||
|
Cursor="Hand"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
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}">
|
||||||
|
<u:RangeTrack.LowerSection>
|
||||||
|
<Border
|
||||||
|
Name="PART_LowerSection"
|
||||||
|
Height="{TemplateBinding TrackWidth}"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="100" />
|
||||||
|
</u:RangeTrack.LowerSection>
|
||||||
|
<u:RangeTrack.LowerThumb>
|
||||||
|
<Thumb
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Theme="{DynamicResource SliderThumbTheme}" />
|
||||||
|
</u:RangeTrack.LowerThumb>
|
||||||
|
<u:RangeTrack.InnerSection>
|
||||||
|
<Border
|
||||||
|
Name="PART_InnerSection"
|
||||||
|
Height="{TemplateBinding TrackWidth}"
|
||||||
|
Background="{TemplateBinding Foreground}"
|
||||||
|
CornerRadius="100" />
|
||||||
|
</u:RangeTrack.InnerSection>
|
||||||
|
<u:RangeTrack.UpperThumb>
|
||||||
|
<Thumb
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Theme="{DynamicResource SliderThumbTheme}" />
|
||||||
|
</u:RangeTrack.UpperThumb>
|
||||||
|
<u:RangeTrack.UpperSection>
|
||||||
|
<Border
|
||||||
|
Name="PART_UpperSection"
|
||||||
|
Height="{TemplateBinding TrackWidth}"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="100" />
|
||||||
|
</u:RangeTrack.UpperSection>
|
||||||
|
</u:RangeTrack>
|
||||||
|
</Grid>
|
||||||
|
</DataValidationErrors>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^:vertical">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:RangeSlider">
|
||||||
|
<DataValidationErrors>
|
||||||
|
<Grid
|
||||||
|
x:Name="SliderContainer"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
Background="{DynamicResource SliderContainerBackground}"
|
||||||
|
ColumnDefinitions="Auto,Auto,Auto">
|
||||||
|
<Grid.Styles>
|
||||||
|
<Style Selector="TickBar">
|
||||||
|
<Setter Property="ReservedSpace" Value="{Binding #PART_Track.LowerThumb.Bounds}" />
|
||||||
|
</Style>
|
||||||
|
</Grid.Styles>
|
||||||
|
<TickBar
|
||||||
|
Name="LeftTickBar"
|
||||||
|
Grid.Column="0"
|
||||||
|
Width="{DynamicResource SliderTickVerticalWidth}"
|
||||||
|
Margin="0,0,4,0"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Fill="{DynamicResource SliderTickForeground}"
|
||||||
|
IsVisible="False"
|
||||||
|
Maximum="{TemplateBinding u:RangeSlider.Maximum}"
|
||||||
|
Minimum="{TemplateBinding u:RangeSlider.Minimum}"
|
||||||
|
Orientation="{TemplateBinding u:RangeSlider.Orientation}"
|
||||||
|
Placement="Left"
|
||||||
|
TickFrequency="{TemplateBinding u:RangeSlider.TickFrequency}"
|
||||||
|
Ticks="{TemplateBinding Ticks}" />
|
||||||
|
<TickBar
|
||||||
|
Name="RightTickBar"
|
||||||
|
Grid.Column="2"
|
||||||
|
Width="{DynamicResource SliderTickVerticalWidth}"
|
||||||
|
Margin="4,0,0,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Fill="{DynamicResource SliderTickForeground}"
|
||||||
|
IsVisible="False"
|
||||||
|
Maximum="{TemplateBinding u:RangeSlider.Maximum}"
|
||||||
|
Minimum="{TemplateBinding u:RangeSlider.Minimum}"
|
||||||
|
Orientation="{TemplateBinding u:RangeSlider.Orientation}"
|
||||||
|
Placement="Right"
|
||||||
|
TickFrequency="{TemplateBinding u:RangeSlider.TickFrequency}"
|
||||||
|
Ticks="{TemplateBinding Ticks}" />
|
||||||
|
<u:RangeTrack
|
||||||
|
Name="{x:Static u:RangeSlider.PART_Track}"
|
||||||
|
Cursor="Hand"
|
||||||
|
Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
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}">
|
||||||
|
<u:RangeTrack.LowerSection>
|
||||||
|
<Border
|
||||||
|
Name="PART_LowerSection"
|
||||||
|
Width="{TemplateBinding TrackWidth}"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="100" />
|
||||||
|
</u:RangeTrack.LowerSection>
|
||||||
|
<u:RangeTrack.LowerThumb>
|
||||||
|
<Thumb
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Theme="{DynamicResource SliderThumbTheme}" />
|
||||||
|
</u:RangeTrack.LowerThumb>
|
||||||
|
<u:RangeTrack.InnerSection>
|
||||||
|
<Border
|
||||||
|
Name="PART_InnerSection"
|
||||||
|
Width="{TemplateBinding TrackWidth}"
|
||||||
|
Background="{TemplateBinding Foreground}"
|
||||||
|
CornerRadius="100" />
|
||||||
|
</u:RangeTrack.InnerSection>
|
||||||
|
<u:RangeTrack.UpperThumb>
|
||||||
|
<Thumb
|
||||||
|
Width="16"
|
||||||
|
Height="16"
|
||||||
|
Theme="{DynamicResource SliderThumbTheme}" />
|
||||||
|
</u:RangeTrack.UpperThumb>
|
||||||
|
<u:RangeTrack.UpperSection>
|
||||||
|
<Border
|
||||||
|
Name="PART_UpperSection"
|
||||||
|
Width="{TemplateBinding TrackWidth}"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="100" />
|
||||||
|
</u:RangeTrack.UpperSection>
|
||||||
|
</u:RangeTrack>
|
||||||
|
</Grid>
|
||||||
|
</DataValidationErrors>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="^[TickPlacement=TopLeft] /template/ TickBar#LeftTickBar, ^[TickPlacement=Outside] /template/ TickBar#LeftTickBar">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="^[TickPlacement=TopLeft] /template/ TickBar#TopTickBar, ^[TickPlacement=Outside] /template/ TickBar#TopTickBar">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="^[TickPlacement=BottomRight] /template/ TickBar#BottomTickBar, ^[TickPlacement=Outside] /template/ TickBar#BottomTickBar">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="^[TickPlacement=BottomRight] /template/ TickBar#RightTickBar, ^[TickPlacement=Outside] /template/ TickBar#RightTickBar">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="^:error /template/ Thumb#thumb">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource DataValidationErrorsSelectedBorderBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="^:disabled">
|
||||||
|
<Style Selector="^ /template/ Border#PART_InnerSection">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SliderTrackDisabledForeground}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="^ /template/ Border#PART_UpperSection, ^ /template/ Border#PART_LowerSection">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SliderTrackDisabledBackground}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="^ /template/ Thumb">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SliderThumbDisabledBorderBrush}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
38
src/Ursa.Themes.Semi/Controls/TwoTonePathIcon.axaml
Normal file
38
src/Ursa.Themes.Semi/Controls/TwoTonePathIcon.axaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<ResourceDictionary
|
||||||
|
xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:u="https://irihi.tech/ursa">
|
||||||
|
<!-- Add Resources Here -->
|
||||||
|
<ControlTheme x:Key="{x:Type u:TwoTonePathIcon}" TargetType="u:TwoTonePathIcon">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Height" Value="{DynamicResource IconElementThemeHeight}" />
|
||||||
|
<Setter Property="Width" Value="{DynamicResource IconElementThemeWidth}" />
|
||||||
|
<Setter Property="StrokeThickness" Value="0.4" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SemiBlue2}" />
|
||||||
|
<Setter Property="StrokeBrush" Value="{DynamicResource SemiBlue6}" />
|
||||||
|
<Setter Property="ActiveForeground" Value="{DynamicResource SemiBlue6}" />
|
||||||
|
<Setter Property="ActiveStrokeBrush" Value="{DynamicResource SemiBlue6}" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate TargetType="u:TwoTonePathIcon">
|
||||||
|
<Border Background="{TemplateBinding Background}">
|
||||||
|
<Panel>
|
||||||
|
<Viewbox Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
|
||||||
|
<Path
|
||||||
|
Name="PART_Fill"
|
||||||
|
Data="{TemplateBinding Data}"
|
||||||
|
StrokeThickness="{TemplateBinding StrokeThickness}"
|
||||||
|
StrokeJoin="Round"
|
||||||
|
Fill="{TemplateBinding Foreground}"
|
||||||
|
Stroke="{TemplateBinding StrokeBrush}"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
</Viewbox>
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
<Style Selector="^:active /template/ Path#PART_Fill">
|
||||||
|
<Setter Property="Fill" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveForeground}" />
|
||||||
|
<Setter Property="Stroke" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveStrokeBrush}" />
|
||||||
|
</Style>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<ResourceInclude Source="DialogShared.axaml" />
|
<ResourceInclude Source="DialogShared.axaml" />
|
||||||
<ResourceInclude Source="Divider.axaml" />
|
<ResourceInclude Source="Divider.axaml" />
|
||||||
<ResourceInclude Source="DualBadge.axaml" />
|
<ResourceInclude Source="DualBadge.axaml" />
|
||||||
|
<ResourceInclude Source="EnumSelector.axaml" />
|
||||||
<ResourceInclude Source="IconButton.axaml" />
|
<ResourceInclude Source="IconButton.axaml" />
|
||||||
<ResourceInclude Source="ImageViewer.axaml" />
|
<ResourceInclude Source="ImageViewer.axaml" />
|
||||||
<ResourceInclude Source="IPv4Box.axaml" />
|
<ResourceInclude Source="IPv4Box.axaml" />
|
||||||
@@ -17,7 +18,9 @@
|
|||||||
<ResourceInclude Source="Navigation.axaml" />
|
<ResourceInclude Source="Navigation.axaml" />
|
||||||
<ResourceInclude Source="NumericUpDown.axaml" />
|
<ResourceInclude Source="NumericUpDown.axaml" />
|
||||||
<ResourceInclude Source="Pagination.axaml" />
|
<ResourceInclude Source="Pagination.axaml" />
|
||||||
|
<ResourceInclude Source="RangeSlider.axaml" />
|
||||||
<ResourceInclude Source="TagInput.axaml" />
|
<ResourceInclude Source="TagInput.axaml" />
|
||||||
<ResourceInclude Source="Timeline.axaml" />
|
<ResourceInclude Source="Timeline.axaml" />
|
||||||
|
<ResourceInclude Source="TwoTonePathIcon.axaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Avalonia;
|
|||||||
using Avalonia.Collections;
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Data;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Metadata;
|
using Avalonia.Metadata;
|
||||||
|
|
||||||
@@ -10,6 +11,40 @@ namespace Ursa.Controls;
|
|||||||
|
|
||||||
public class ButtonGroup: ItemsControl
|
public class ButtonGroup: ItemsControl
|
||||||
{
|
{
|
||||||
|
public static readonly StyledProperty<IBinding?> CommandBindingProperty = AvaloniaProperty.Register<ButtonGroup, IBinding?>(
|
||||||
|
nameof(CommandBinding));
|
||||||
|
|
||||||
|
[AssignBinding]
|
||||||
|
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||||
|
public IBinding? CommandBinding
|
||||||
|
{
|
||||||
|
get => GetValue(CommandBindingProperty);
|
||||||
|
set => SetValue(CommandBindingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBinding?> CommandParameterBindingProperty = AvaloniaProperty.Register<ButtonGroup, IBinding?>(
|
||||||
|
nameof(CommandParameterBinding));
|
||||||
|
|
||||||
|
[AssignBinding]
|
||||||
|
[InheritDataTypeFromItems(nameof(ItemsSource))]
|
||||||
|
public IBinding? CommandParameterBinding
|
||||||
|
{
|
||||||
|
get => GetValue(CommandParameterBindingProperty);
|
||||||
|
set => SetValue(CommandParameterBindingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBinding?> ContentBindingProperty = AvaloniaProperty.Register<ButtonGroup, IBinding?>(
|
||||||
|
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)
|
protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
|
||||||
{
|
{
|
||||||
recycleKey = null;
|
recycleKey = null;
|
||||||
@@ -20,4 +55,28 @@ public class ButtonGroup: ItemsControl
|
|||||||
{
|
{
|
||||||
return new Button();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
158
src/Ursa/Controls/EnumSelector/EnumSelector.cs
Normal file
158
src/Ursa/Controls/EnumSelector/EnumSelector.cs
Normal file
@@ -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<Type?> EnumTypeProperty = AvaloniaProperty.Register<EnumSelector, Type?>(
|
||||||
|
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<object?> ValueProperty = AvaloniaProperty.Register<EnumSelector, object?>(
|
||||||
|
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<EnumSelector, EnumItemTuple?> SelectedValueProperty = AvaloniaProperty.RegisterDirect<EnumSelector, EnumItemTuple?>(
|
||||||
|
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<EnumSelector, IList<EnumItemTuple>?> ValuesProperty = AvaloniaProperty.RegisterDirect<EnumSelector, IList<EnumItemTuple>?>(
|
||||||
|
nameof(Values), o => o.Values);
|
||||||
|
|
||||||
|
private IList<EnumItemTuple>? _values;
|
||||||
|
internal IList<EnumItemTuple>? Values
|
||||||
|
{
|
||||||
|
get => _values;
|
||||||
|
private set => SetAndRaise(ValuesProperty, ref _values, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> DisplayDescriptionProperty = AvaloniaProperty.Register<EnumSelector, bool>(
|
||||||
|
nameof(DisplayDescription));
|
||||||
|
|
||||||
|
public bool DisplayDescription
|
||||||
|
{
|
||||||
|
get => GetValue(DisplayDescriptionProperty);
|
||||||
|
set => SetValue(DisplayDescriptionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static EnumSelector()
|
||||||
|
{
|
||||||
|
EnumTypeProperty.Changed.AddClassHandler<EnumSelector, Type?>((o, e) => o.OnTypeChanged(e));
|
||||||
|
SelectedValueProperty.Changed.AddClassHandler<EnumSelector, EnumItemTuple?>((o, e) => o.OnSelectedValueChanged(e));
|
||||||
|
ValueProperty.Changed.AddClassHandler<EnumSelector, object?>((o, e) => o.OnValueChanged(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<object?> 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<EnumItemTuple?> args)
|
||||||
|
{
|
||||||
|
_updateFromComboBox = true;
|
||||||
|
var newValue = args.NewValue.Value;
|
||||||
|
SetCurrentValue(ValueProperty, newValue?.Value);
|
||||||
|
_updateFromComboBox = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void OnTypeChanged(AvaloniaPropertyChangedEventArgs<Type?> args)
|
||||||
|
{
|
||||||
|
Values?.Clear();
|
||||||
|
var newType = args.GetNewValue<Type?>();
|
||||||
|
if (newType is null || !newType.IsEnum)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Values = GenerateItemTuple();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<EnumItemTuple> GenerateItemTuple()
|
||||||
|
{
|
||||||
|
if (EnumType is null) return new List<EnumItemTuple>();
|
||||||
|
var values = Enum.GetValues(EnumType);
|
||||||
|
List<EnumItemTuple> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/Ursa/Controls/Icons/TwoTonePathIcon.cs
Normal file
92
src/Ursa/Controls/Icons/TwoTonePathIcon.cs
Normal file
@@ -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<IBrush?> StrokeBrushProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||||
|
nameof(StrokeBrush));
|
||||||
|
|
||||||
|
public IBrush? StrokeBrush
|
||||||
|
{
|
||||||
|
get => GetValue(StrokeBrushProperty);
|
||||||
|
set => SetValue(StrokeBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Geometry> DataProperty = AvaloniaProperty.Register<PathIcon, Geometry>(
|
||||||
|
nameof(Data));
|
||||||
|
|
||||||
|
public Geometry Data
|
||||||
|
{
|
||||||
|
get => GetValue(DataProperty);
|
||||||
|
set => SetValue(DataProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsActiveProperty = AvaloniaProperty.Register<TwoTonePathIcon, bool>(
|
||||||
|
nameof(IsActive), defaultBindingMode: BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public bool IsActive
|
||||||
|
{
|
||||||
|
get => GetValue(IsActiveProperty);
|
||||||
|
set => SetValue(IsActiveProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush?> ActiveForegroundProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||||
|
nameof(ActiveForeground));
|
||||||
|
|
||||||
|
public IBrush? ActiveForeground
|
||||||
|
{
|
||||||
|
get => GetValue(ActiveForegroundProperty);
|
||||||
|
set => SetValue(ActiveForegroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush?> ActiveStrokeBrushProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||||
|
nameof(ActiveStrokeBrush));
|
||||||
|
|
||||||
|
public IBrush? ActiveStrokeBrush
|
||||||
|
{
|
||||||
|
get => GetValue(ActiveStrokeBrushProperty);
|
||||||
|
set => SetValue(ActiveStrokeBrushProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> StrokeThicknessProperty =
|
||||||
|
AvaloniaProperty.Register<TwoTonePathIcon, double>(
|
||||||
|
nameof(StrokeThickness));
|
||||||
|
public double StrokeThickness
|
||||||
|
{
|
||||||
|
get => GetValue(StrokeThicknessProperty);
|
||||||
|
set => SetValue(StrokeThicknessProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TwoTonePathIcon()
|
||||||
|
{
|
||||||
|
AffectsRender<TwoTonePathIcon>(
|
||||||
|
DataProperty,
|
||||||
|
StrokeBrushProperty,
|
||||||
|
ForegroundProperty,
|
||||||
|
ActiveForegroundProperty,
|
||||||
|
ActiveStrokeBrushProperty);
|
||||||
|
IsActiveProperty.Changed.AddClassHandler<TwoTonePathIcon, bool>((o, e) => o.OnIsActiveChanged(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnIsActiveChanged(AvaloniaPropertyChangedEventArgs<bool> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ using Avalonia;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Metadata;
|
using Avalonia.Controls.Metadata;
|
||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Data;
|
||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
@@ -288,7 +289,7 @@ public abstract class NumericUpDown : TemplatedControl
|
|||||||
public abstract class NumericUpDownBase<T>: NumericUpDown where T: struct, IComparable<T>
|
public abstract class NumericUpDownBase<T>: NumericUpDown where T: struct, IComparable<T>
|
||||||
{
|
{
|
||||||
public static readonly StyledProperty<T?> ValueProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T?>(
|
public static readonly StyledProperty<T?> ValueProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T?>(
|
||||||
nameof(Value));
|
nameof(Value), defaultBindingMode: BindingMode.TwoWay);
|
||||||
|
|
||||||
public T? Value
|
public T? Value
|
||||||
{
|
{
|
||||||
@@ -297,7 +298,7 @@ public abstract class NumericUpDownBase<T>: NumericUpDown where T: struct, IComp
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<T> MaximumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
public static readonly StyledProperty<T> MaximumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||||
nameof(Maximum), coerce: CoerceMaximum);
|
nameof(Maximum), defaultBindingMode:BindingMode.TwoWay, coerce: CoerceMaximum);
|
||||||
|
|
||||||
public T Maximum
|
public T Maximum
|
||||||
{
|
{
|
||||||
@@ -306,7 +307,7 @@ public abstract class NumericUpDownBase<T>: NumericUpDown where T: struct, IComp
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<T> MinimumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
public static readonly StyledProperty<T> MinimumProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T>(
|
||||||
nameof(Minimum), coerce: CoerceMinimum);
|
nameof(Minimum), defaultBindingMode:BindingMode.TwoWay, coerce: CoerceMinimum);
|
||||||
|
|
||||||
public T Minimum
|
public T Minimum
|
||||||
{
|
{
|
||||||
|
|||||||
304
src/Ursa/Controls/RangeSlider/RangeSlider.cs
Normal file
304
src/Ursa/Controls/RangeSlider/RangeSlider.cs
Normal file
@@ -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<double> MinimumProperty = RangeTrack.MinimumProperty.AddOwner<RangeSlider>();
|
||||||
|
public double Minimum
|
||||||
|
{
|
||||||
|
get => GetValue(MinimumProperty);
|
||||||
|
set => SetValue(MinimumProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> MaximumProperty = RangeTrack.MaximumProperty.AddOwner<RangeSlider>();
|
||||||
|
public double Maximum
|
||||||
|
{
|
||||||
|
get => GetValue(MaximumProperty);
|
||||||
|
set => SetValue(MaximumProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> LowerValueProperty = RangeTrack.LowerValueProperty.AddOwner<RangeSlider>();
|
||||||
|
public double LowerValue
|
||||||
|
{
|
||||||
|
get => GetValue(LowerValueProperty);
|
||||||
|
set => SetValue(LowerValueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> UpperValueProperty = RangeTrack.UpperValueProperty.AddOwner<RangeSlider>();
|
||||||
|
public double UpperValue
|
||||||
|
{
|
||||||
|
get => GetValue(UpperValueProperty);
|
||||||
|
set => SetValue(UpperValueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> TrackWidthProperty = AvaloniaProperty.Register<RangeSlider, double>(
|
||||||
|
nameof(TrackWidth));
|
||||||
|
|
||||||
|
public double TrackWidth
|
||||||
|
{
|
||||||
|
get => GetValue(TrackWidthProperty);
|
||||||
|
set => SetValue(TrackWidthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Orientation> OrientationProperty = RangeTrack.OrientationProperty.AddOwner<RangeSlider>();
|
||||||
|
|
||||||
|
public Orientation Orientation
|
||||||
|
{
|
||||||
|
get => GetValue(OrientationProperty);
|
||||||
|
set => SetValue(OrientationProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsDirectionReversedProperty =
|
||||||
|
RangeTrack.IsDirectionReversedProperty.AddOwner<RangeSlider>();
|
||||||
|
|
||||||
|
public bool IsDirectionReversed
|
||||||
|
{
|
||||||
|
get => GetValue(IsDirectionReversedProperty);
|
||||||
|
set => SetValue(IsDirectionReversedProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> TickFrequencyProperty = AvaloniaProperty.Register<RangeSlider, double>(
|
||||||
|
nameof(TickFrequency));
|
||||||
|
|
||||||
|
public double TickFrequency
|
||||||
|
{
|
||||||
|
get => GetValue(TickFrequencyProperty);
|
||||||
|
set => SetValue(TickFrequencyProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<AvaloniaList<double>?> TicksProperty =
|
||||||
|
TickBar.TicksProperty.AddOwner<RangeSlider>();
|
||||||
|
|
||||||
|
public AvaloniaList<double>? Ticks
|
||||||
|
{
|
||||||
|
get => GetValue(TicksProperty);
|
||||||
|
set => SetValue(TicksProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
|
||||||
|
Slider.TickPlacementProperty.AddOwner<RangeSlider>();
|
||||||
|
|
||||||
|
public TickPlacement TickPlacement
|
||||||
|
{
|
||||||
|
get => GetValue(TickPlacementProperty);
|
||||||
|
set => SetValue(TickPlacementProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsSnapToTickProperty = AvaloniaProperty.Register<RangeSlider, bool>(
|
||||||
|
nameof(IsSnapToTick));
|
||||||
|
|
||||||
|
public bool IsSnapToTick
|
||||||
|
{
|
||||||
|
get => GetValue(IsSnapToTickProperty);
|
||||||
|
set => SetValue(IsSnapToTickProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly RoutedEvent<RangeValueChangedEventArgs> ValueChangedEvent =
|
||||||
|
RoutedEvent.Register<RangeSlider, RangeValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||||
|
|
||||||
|
public event EventHandler<RangeValueChangedEventArgs> ValueChanged
|
||||||
|
{
|
||||||
|
add => AddHandler(ValueChangedEvent, value);
|
||||||
|
remove => RemoveHandler(ValueChangedEvent, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RangeSlider()
|
||||||
|
{
|
||||||
|
PressedMixin.Attach<RangeSlider>();
|
||||||
|
FocusableProperty.OverrideDefaultValue<RangeSlider>(true);
|
||||||
|
IsHitTestVisibleProperty.OverrideDefaultValue<RangeSlider>(true);
|
||||||
|
OrientationProperty.OverrideDefaultValue<RangeSlider>(Orientation.Horizontal);
|
||||||
|
OrientationProperty.Changed.AddClassHandler<RangeSlider, Orientation>((o,e)=>o.OnOrientationChanged(e));
|
||||||
|
MinimumProperty.OverrideDefaultValue<RangeSlider>(0);
|
||||||
|
MaximumProperty.OverrideDefaultValue<RangeSlider>(100);
|
||||||
|
LowerValueProperty.OverrideDefaultValue<RangeSlider>(0);
|
||||||
|
UpperValueProperty.OverrideDefaultValue<RangeSlider>(100);
|
||||||
|
LowerValueProperty.Changed.AddClassHandler<RangeSlider, double>((o, e) => o.OnValueChanged(e, true));
|
||||||
|
UpperValueProperty.Changed.AddClassHandler<RangeSlider, double>((o, e) => o.OnValueChanged(e, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> 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<Orientation> 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<RangeTrack>(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<upperDistance)
|
||||||
|
{
|
||||||
|
return _track?.LowerThumb;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _track?.UpperThumb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetValueByPoint(PointerPoint point)
|
||||||
|
{
|
||||||
|
if (_track is null) return 0;
|
||||||
|
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePseudoClasses(Orientation o)
|
||||||
|
{
|
||||||
|
this.PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical);
|
||||||
|
this.PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal);
|
||||||
|
}
|
||||||
|
}
|
||||||
506
src/Ursa/Controls/RangeSlider/RangeTrack.cs
Normal file
506
src/Ursa/Controls/RangeSlider/RangeTrack.cs
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
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;
|
||||||
|
using Avalonia.Utilities;
|
||||||
|
|
||||||
|
namespace Ursa.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[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<double> MinimumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||||
|
nameof(Minimum), coerce: CoerceMinimum, defaultBindingMode:BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public double Minimum
|
||||||
|
{
|
||||||
|
get => GetValue(MinimumProperty);
|
||||||
|
set => SetValue(MinimumProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> MaximumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||||
|
nameof(Maximum), coerce: CoerceMaximum, defaultBindingMode: BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public double Maximum
|
||||||
|
{
|
||||||
|
get => GetValue(MaximumProperty);
|
||||||
|
set => SetValue(MaximumProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> LowerValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||||
|
nameof(LowerValue), coerce: CoerceLowerValue, defaultBindingMode: BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public double LowerValue
|
||||||
|
{
|
||||||
|
get => GetValue(LowerValueProperty);
|
||||||
|
set => SetValue(LowerValueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> UpperValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||||
|
nameof(UpperValue), coerce: CoerceUpperValue, defaultBindingMode: BindingMode.TwoWay);
|
||||||
|
|
||||||
|
public double UpperValue
|
||||||
|
{
|
||||||
|
get => GetValue(UpperValueProperty);
|
||||||
|
set => SetValue(UpperValueProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Orientation> OrientationProperty = AvaloniaProperty.Register<RangeTrack, Orientation>(
|
||||||
|
nameof(Orientation));
|
||||||
|
|
||||||
|
public Orientation Orientation
|
||||||
|
{
|
||||||
|
get => GetValue(OrientationProperty);
|
||||||
|
set => SetValue(OrientationProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Control?> UpperSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||||
|
nameof(UpperSection));
|
||||||
|
|
||||||
|
public Control? UpperSection
|
||||||
|
{
|
||||||
|
get => GetValue(UpperSectionProperty);
|
||||||
|
set => SetValue(UpperSectionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Control?> LowerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||||
|
nameof(LowerSection));
|
||||||
|
|
||||||
|
public Control? LowerSection
|
||||||
|
{
|
||||||
|
get => GetValue(LowerSectionProperty);
|
||||||
|
set => SetValue(LowerSectionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Control?> InnerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||||
|
nameof(InnerSection));
|
||||||
|
|
||||||
|
public Control? InnerSection
|
||||||
|
{
|
||||||
|
get => GetValue(InnerSectionProperty);
|
||||||
|
set => SetValue(InnerSectionProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Control?> TrackBackgroundProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||||
|
nameof(TrackBackground));
|
||||||
|
|
||||||
|
public Control? TrackBackground
|
||||||
|
{
|
||||||
|
get => GetValue(TrackBackgroundProperty);
|
||||||
|
set => SetValue(TrackBackgroundProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Thumb?> UpperThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
||||||
|
nameof(UpperThumb));
|
||||||
|
|
||||||
|
public Thumb? UpperThumb
|
||||||
|
{
|
||||||
|
get => GetValue(UpperThumbProperty);
|
||||||
|
set => SetValue(UpperThumbProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<Thumb?> LowerThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
||||||
|
nameof(LowerThumb));
|
||||||
|
|
||||||
|
public Thumb? LowerThumb
|
||||||
|
{
|
||||||
|
get => GetValue(LowerThumbProperty);
|
||||||
|
set => SetValue(LowerThumbProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> IsDirectionReversedProperty = AvaloniaProperty.Register<RangeTrack, bool>(
|
||||||
|
nameof(IsDirectionReversed));
|
||||||
|
|
||||||
|
public bool IsDirectionReversed
|
||||||
|
{
|
||||||
|
get => GetValue(IsDirectionReversedProperty);
|
||||||
|
set => SetValue(IsDirectionReversedProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly RoutedEvent<RangeValueChangedEventArgs> ValueChangedEvent =
|
||||||
|
RoutedEvent.Register<RangeTrack, RangeValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||||
|
|
||||||
|
public event EventHandler<RangeValueChangedEventArgs> ValueChanged
|
||||||
|
{
|
||||||
|
add => AddHandler(ValueChangedEvent, value);
|
||||||
|
remove => RemoveHandler(ValueChangedEvent, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RangeTrack()
|
||||||
|
{
|
||||||
|
OrientationProperty.Changed.AddClassHandler<RangeTrack, Orientation>((o, e) => o.OnOrientationChanged(e));
|
||||||
|
LowerThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
||||||
|
UpperThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
||||||
|
LowerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||||
|
UpperSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||||
|
InnerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||||
|
MinimumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMinimumChanged(e));
|
||||||
|
MaximumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMaximumChanged(e));
|
||||||
|
LowerValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, true));
|
||||||
|
UpperValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, false));
|
||||||
|
AffectsArrange<RangeTrack>(
|
||||||
|
MinimumProperty,
|
||||||
|
MaximumProperty,
|
||||||
|
LowerValueProperty,
|
||||||
|
UpperValueProperty,
|
||||||
|
OrientationProperty,
|
||||||
|
IsDirectionReversedProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> 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<double> avaloniaPropertyChangedEventArgs)
|
||||||
|
{
|
||||||
|
if (IsInitialized)
|
||||||
|
{
|
||||||
|
CoerceValue(MaximumProperty);
|
||||||
|
CoerceValue(LowerValueProperty);
|
||||||
|
CoerceValue(UpperValueProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMaximumChanged(AvaloniaPropertyChangedEventArgs<double> avaloniaPropertyChangedEventArgs)
|
||||||
|
{
|
||||||
|
if (IsInitialized)
|
||||||
|
{
|
||||||
|
CoerceValue(LowerValueProperty);
|
||||||
|
CoerceValue(UpperValueProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSectionChanged(AvaloniaPropertyChangedEventArgs<Control?> 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<Thumb?> 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<Orientation> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs
Normal file
34
src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user