@@ -17,6 +17,7 @@ public static class MenuKeys
|
||||
public const string MenuKeyNavigation = "Navigation";
|
||||
public const string MenuKeyNumericUpDown = "NumericUpDown";
|
||||
public const string MenuKeyPagination = "Pagination";
|
||||
public const string MenuKeyRangeSlider = "RangeSlider";
|
||||
public const string MenuKeyTagInput = "TagInput";
|
||||
public const string MenuKeyTimeline = "Timeline";
|
||||
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public class MainViewViewModel : ViewModelBase
|
||||
MenuKeys.MenuKeyNavigation => new NavigationMenuDemoViewModel(),
|
||||
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
|
||||
MenuKeys.MenuKeyPagination => new PaginationDemoViewModel(),
|
||||
MenuKeys.MenuKeyRangeSlider => new RangeSliderDemoViewModel(),
|
||||
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
||||
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
||||
};
|
||||
|
||||
@@ -5,11 +5,19 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Ursa.Demo.ViewModels;
|
||||
|
||||
public enum ControlStatus
|
||||
{
|
||||
New,
|
||||
Beta,
|
||||
Stable,
|
||||
}
|
||||
|
||||
public class MenuItemViewModel: ViewModelBase
|
||||
{
|
||||
public string MenuHeader { get; set; }
|
||||
public string MenuIconName { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Status { get; set; }
|
||||
|
||||
public bool IsSeparator { get; set; }
|
||||
public ObservableCollection<MenuItemViewModel> Children { get; set; } = new();
|
||||
|
||||
@@ -22,12 +22,13 @@ public class MenuViewModel: ViewModelBase
|
||||
new() { MenuHeader = "IPv4Box", Key = MenuKeys.MenuKeyIpBox },
|
||||
new() { MenuHeader = "KeyGestureInput", Key = MenuKeys.MenuKeyKeyGestureInput },
|
||||
new() { MenuHeader = "Loading", Key = MenuKeys.MenuKeyLoading },
|
||||
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox },
|
||||
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation },
|
||||
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown },
|
||||
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox, Status = "New" },
|
||||
new() { MenuHeader = "Navigation", Key = MenuKeys.MenuKeyNavigation, Status = "WIP" },
|
||||
new() { MenuHeader = "NumericUpDown", Key = MenuKeys.MenuKeyNumericUpDown, Status = "New" },
|
||||
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
||||
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"},
|
||||
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
|
||||
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
|
||||
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" },
|
||||
};
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -43,8 +43,21 @@
|
||||
<DataTemplate DataType="vm:MenuItemViewModel">
|
||||
<u:NavigationMenuItem
|
||||
Command="{Binding ActivateCommand}"
|
||||
Header="{Binding MenuHeader}"
|
||||
Header="{Binding}"
|
||||
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>
|
||||
<Border
|
||||
Width="10"
|
||||
|
||||
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>
|
||||
@@ -15,6 +15,7 @@
|
||||
<ResourceInclude Source="Navigation.axaml" />
|
||||
<ResourceInclude Source="NumericUpDown.axaml" />
|
||||
<ResourceInclude Source="Pagination.axaml" />
|
||||
<ResourceInclude Source="RangeSlider.axaml" />
|
||||
<ResourceInclude Source="TagInput.axaml" />
|
||||
<ResourceInclude Source="Timeline.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
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