Merge branch 'main' into dialog

This commit is contained in:
Dong Bin
2024-01-27 14:07:04 +08:00
committed by GitHub
28 changed files with 1772 additions and 16 deletions

View File

@@ -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";
} }

View File

@@ -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>

View 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>

View 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();
}
}

View 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>

View 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();
}
}

View 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>

View 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();
}
}

View File

@@ -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);
}
} }

View 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,
}

View File

@@ -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(),
}; };
} }
} }

View File

@@ -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();

View File

@@ -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"},
}; };
} }
} }

View 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;
}

View File

@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ursa.Demo.ViewModels;
public class TwoTonePathIconDemoViewModel:ObservableObject
{
}

View File

@@ -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"

View File

@@ -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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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;
}
}
}
} }

View 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;
}
}

View 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);
}
}

View File

@@ -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
{ {

View 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);
}
}

View 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;
}
}
}
}

View 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;
}
}