Merge pull request #70 from irihitech/slider

New Control: RangeSlider
This commit is contained in:
Zhang Dian
2024-01-24 11:53:11 +08:00
committed by GitHub
13 changed files with 1185 additions and 5 deletions

View File

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

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

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

View File

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

View File

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

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

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

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

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

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