Merge branch 'main' into dialog
This commit is contained in:
@@ -3,6 +3,7 @@ using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
@@ -10,6 +11,40 @@ namespace Ursa.Controls;
|
||||
|
||||
public class ButtonGroup: ItemsControl
|
||||
{
|
||||
public static readonly StyledProperty<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)
|
||||
{
|
||||
recycleKey = null;
|
||||
@@ -20,4 +55,28 @@ public class ButtonGroup: ItemsControl
|
||||
{
|
||||
return new Button();
|
||||
}
|
||||
|
||||
protected override void PrepareContainerForItemOverride(Control container, object? item, int index)
|
||||
{
|
||||
base.PrepareContainerForItemOverride(container, item, index);
|
||||
if(container is Button button)
|
||||
{
|
||||
if ( CommandBinding is not null)
|
||||
{
|
||||
button[!Button.CommandProperty] = CommandBinding;
|
||||
}
|
||||
if ( CommandParameterBinding is not null)
|
||||
{
|
||||
button[!Button.CommandParameterProperty] = CommandParameterBinding;
|
||||
}
|
||||
if ( ContentBinding is not null)
|
||||
{
|
||||
button[!Button.ContentProperty] = ContentBinding;
|
||||
}
|
||||
if (ItemTemplate is not null)
|
||||
{
|
||||
button.ContentTemplate = ItemTemplate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/Ursa/Controls/EnumSelector/EnumSelector.cs
Normal file
158
src/Ursa/Controls/EnumSelector/EnumSelector.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class EnumItemTuple
|
||||
{
|
||||
public string? DisplayName { get; set; }
|
||||
public object? Value { get; set; }
|
||||
}
|
||||
|
||||
public class EnumSelector: TemplatedControl
|
||||
{
|
||||
public static readonly StyledProperty<Type?> EnumTypeProperty = AvaloniaProperty.Register<EnumSelector, Type?>(
|
||||
nameof(EnumType), validate: OnTypeValidate);
|
||||
|
||||
public Type? EnumType
|
||||
{
|
||||
get => GetValue(EnumTypeProperty);
|
||||
set => SetValue(EnumTypeProperty, value);
|
||||
}
|
||||
|
||||
private static bool OnTypeValidate(Type? arg)
|
||||
{
|
||||
if (arg is null) return true;
|
||||
return arg.IsEnum;
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<object?> ValueProperty = AvaloniaProperty.Register<EnumSelector, object?>(
|
||||
nameof(Value), defaultBindingMode: BindingMode.TwoWay, coerce:OnValueCoerce);
|
||||
|
||||
private static object? OnValueCoerce(AvaloniaObject o, object? value)
|
||||
{
|
||||
if (o is not EnumSelector selector) return null;
|
||||
if (value is null) return null;
|
||||
if (value.GetType() != selector.EnumType) return null;
|
||||
var first = selector.Values.FirstOrDefault(a => Equals(a.Value, value));
|
||||
if (first is null) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
public object? Value
|
||||
{
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
|
||||
private EnumItemTuple? _selectedValue;
|
||||
|
||||
public static readonly DirectProperty<EnumSelector, EnumItemTuple?> SelectedValueProperty = AvaloniaProperty.RegisterDirect<EnumSelector, EnumItemTuple?>(
|
||||
nameof(SelectedValue), o => o.SelectedValue, (o, v) => o.SelectedValue = v);
|
||||
|
||||
public EnumItemTuple? SelectedValue
|
||||
{
|
||||
get => _selectedValue;
|
||||
private set => SetAndRaise(SelectedValueProperty, ref _selectedValue, value);
|
||||
}
|
||||
|
||||
public static readonly DirectProperty<EnumSelector, IList<EnumItemTuple>?> ValuesProperty = AvaloniaProperty.RegisterDirect<EnumSelector, IList<EnumItemTuple>?>(
|
||||
nameof(Values), o => o.Values);
|
||||
|
||||
private IList<EnumItemTuple>? _values;
|
||||
internal IList<EnumItemTuple>? Values
|
||||
{
|
||||
get => _values;
|
||||
private set => SetAndRaise(ValuesProperty, ref _values, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> DisplayDescriptionProperty = AvaloniaProperty.Register<EnumSelector, bool>(
|
||||
nameof(DisplayDescription));
|
||||
|
||||
public bool DisplayDescription
|
||||
{
|
||||
get => GetValue(DisplayDescriptionProperty);
|
||||
set => SetValue(DisplayDescriptionProperty, value);
|
||||
}
|
||||
|
||||
static EnumSelector()
|
||||
{
|
||||
EnumTypeProperty.Changed.AddClassHandler<EnumSelector, Type?>((o, e) => o.OnTypeChanged(e));
|
||||
SelectedValueProperty.Changed.AddClassHandler<EnumSelector, EnumItemTuple?>((o, e) => o.OnSelectedValueChanged(e));
|
||||
ValueProperty.Changed.AddClassHandler<EnumSelector, object?>((o, e) => o.OnValueChanged(e));
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<object?> args)
|
||||
{
|
||||
if (_updateFromComboBox) return;
|
||||
var newValue = args.NewValue.Value;
|
||||
if (newValue is null)
|
||||
{
|
||||
SetCurrentValue(SelectedValueProperty, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (newValue.GetType() != EnumType)
|
||||
{
|
||||
SetCurrentValue(SelectedValueProperty, null);
|
||||
}
|
||||
var tuple = Values?.FirstOrDefault(x => Equals(x.Value, newValue));
|
||||
SetCurrentValue(SelectedValueProperty, tuple);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _updateFromComboBox;
|
||||
|
||||
private void OnSelectedValueChanged(AvaloniaPropertyChangedEventArgs<EnumItemTuple?> args)
|
||||
{
|
||||
_updateFromComboBox = true;
|
||||
var newValue = args.NewValue.Value;
|
||||
SetCurrentValue(ValueProperty, newValue?.Value);
|
||||
_updateFromComboBox = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void OnTypeChanged(AvaloniaPropertyChangedEventArgs<Type?> args)
|
||||
{
|
||||
Values?.Clear();
|
||||
var newType = args.GetNewValue<Type?>();
|
||||
if (newType is null || !newType.IsEnum)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Values = GenerateItemTuple();
|
||||
}
|
||||
|
||||
private List<EnumItemTuple> GenerateItemTuple()
|
||||
{
|
||||
if (EnumType is null) return new List<EnumItemTuple>();
|
||||
var values = Enum.GetValues(EnumType);
|
||||
List<EnumItemTuple> list = new();
|
||||
var fields = EnumType.GetFields();
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (value.GetType() == EnumType)
|
||||
{
|
||||
var displayName = value.ToString();
|
||||
var field = EnumType.GetField(displayName);
|
||||
var description = field?.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault();
|
||||
if (description is not null)
|
||||
{
|
||||
displayName = ((DescriptionAttribute) description).Description;
|
||||
}
|
||||
list.Add(new EnumItemTuple()
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
92
src/Ursa/Controls/Icons/TwoTonePathIcon.cs
Normal file
92
src/Ursa/Controls/Icons/TwoTonePathIcon.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[PseudoClasses(PC_Active)]
|
||||
public class TwoTonePathIcon: TemplatedControl
|
||||
{
|
||||
public const string PC_Active = ":active";
|
||||
|
||||
public static readonly StyledProperty<IBrush?> StrokeBrushProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||
nameof(StrokeBrush));
|
||||
|
||||
public IBrush? StrokeBrush
|
||||
{
|
||||
get => GetValue(StrokeBrushProperty);
|
||||
set => SetValue(StrokeBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Geometry> DataProperty = AvaloniaProperty.Register<PathIcon, Geometry>(
|
||||
nameof(Data));
|
||||
|
||||
public Geometry Data
|
||||
{
|
||||
get => GetValue(DataProperty);
|
||||
set => SetValue(DataProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsActiveProperty = AvaloniaProperty.Register<TwoTonePathIcon, bool>(
|
||||
nameof(IsActive), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public bool IsActive
|
||||
{
|
||||
get => GetValue(IsActiveProperty);
|
||||
set => SetValue(IsActiveProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> ActiveForegroundProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||
nameof(ActiveForeground));
|
||||
|
||||
public IBrush? ActiveForeground
|
||||
{
|
||||
get => GetValue(ActiveForegroundProperty);
|
||||
set => SetValue(ActiveForegroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<IBrush?> ActiveStrokeBrushProperty = AvaloniaProperty.Register<TwoTonePathIcon, IBrush?>(
|
||||
nameof(ActiveStrokeBrush));
|
||||
|
||||
public IBrush? ActiveStrokeBrush
|
||||
{
|
||||
get => GetValue(ActiveStrokeBrushProperty);
|
||||
set => SetValue(ActiveStrokeBrushProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> StrokeThicknessProperty =
|
||||
AvaloniaProperty.Register<TwoTonePathIcon, double>(
|
||||
nameof(StrokeThickness));
|
||||
public double StrokeThickness
|
||||
{
|
||||
get => GetValue(StrokeThicknessProperty);
|
||||
set => SetValue(StrokeThicknessProperty, value);
|
||||
}
|
||||
|
||||
static TwoTonePathIcon()
|
||||
{
|
||||
AffectsRender<TwoTonePathIcon>(
|
||||
DataProperty,
|
||||
StrokeBrushProperty,
|
||||
ForegroundProperty,
|
||||
ActiveForegroundProperty,
|
||||
ActiveStrokeBrushProperty);
|
||||
IsActiveProperty.Changed.AddClassHandler<TwoTonePathIcon, bool>((o, e) => o.OnIsActiveChanged(e));
|
||||
}
|
||||
|
||||
private void OnIsActiveChanged(AvaloniaPropertyChangedEventArgs<bool> args)
|
||||
{
|
||||
var newValue = args.NewValue.Value;
|
||||
PseudoClasses.Set(PC_Active, newValue);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
PseudoClasses.Set(PC_Active, IsActive);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -288,7 +289,7 @@ public abstract class NumericUpDown : TemplatedControl
|
||||
public abstract class NumericUpDownBase<T>: NumericUpDown where T: struct, IComparable<T>
|
||||
{
|
||||
public static readonly StyledProperty<T?> ValueProperty = AvaloniaProperty.Register<NumericUpDownBase<T>, T?>(
|
||||
nameof(Value));
|
||||
nameof(Value), defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
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>(
|
||||
nameof(Maximum), coerce: CoerceMaximum);
|
||||
nameof(Maximum), defaultBindingMode:BindingMode.TwoWay, coerce: CoerceMaximum);
|
||||
|
||||
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>(
|
||||
nameof(Minimum), coerce: CoerceMinimum);
|
||||
nameof(Minimum), defaultBindingMode:BindingMode.TwoWay, coerce: CoerceMinimum);
|
||||
|
||||
public T Minimum
|
||||
{
|
||||
|
||||
304
src/Ursa/Controls/RangeSlider/RangeSlider.cs
Normal file
304
src/Ursa/Controls/RangeSlider/RangeSlider.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Utilities;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_Track, typeof(RangeTrack))]
|
||||
[PseudoClasses(PC_Horizontal, PC_Vertical, PC_Pressed)]
|
||||
public class RangeSlider: TemplatedControl
|
||||
{
|
||||
public const string PART_Track = "PART_Track";
|
||||
private const string PC_Horizontal= ":horizontal";
|
||||
private const string PC_Vertical = ":vertical";
|
||||
private const string PC_Pressed = ":pressed";
|
||||
|
||||
private RangeTrack? _track;
|
||||
private bool _isDragging;
|
||||
private IDisposable? _pointerPressedDisposable;
|
||||
private IDisposable? _pointerMoveDisposable;
|
||||
private IDisposable? _pointerReleasedDisposable;
|
||||
|
||||
private const double Tolerance = 0.0001;
|
||||
|
||||
public static readonly StyledProperty<double> MinimumProperty = RangeTrack.MinimumProperty.AddOwner<RangeSlider>();
|
||||
public double Minimum
|
||||
{
|
||||
get => GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MaximumProperty = RangeTrack.MaximumProperty.AddOwner<RangeSlider>();
|
||||
public double Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> LowerValueProperty = RangeTrack.LowerValueProperty.AddOwner<RangeSlider>();
|
||||
public double LowerValue
|
||||
{
|
||||
get => GetValue(LowerValueProperty);
|
||||
set => SetValue(LowerValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> UpperValueProperty = RangeTrack.UpperValueProperty.AddOwner<RangeSlider>();
|
||||
public double UpperValue
|
||||
{
|
||||
get => GetValue(UpperValueProperty);
|
||||
set => SetValue(UpperValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> TrackWidthProperty = AvaloniaProperty.Register<RangeSlider, double>(
|
||||
nameof(TrackWidth));
|
||||
|
||||
public double TrackWidth
|
||||
{
|
||||
get => GetValue(TrackWidthProperty);
|
||||
set => SetValue(TrackWidthProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Orientation> OrientationProperty = RangeTrack.OrientationProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsDirectionReversedProperty =
|
||||
RangeTrack.IsDirectionReversedProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public bool IsDirectionReversed
|
||||
{
|
||||
get => GetValue(IsDirectionReversedProperty);
|
||||
set => SetValue(IsDirectionReversedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> TickFrequencyProperty = AvaloniaProperty.Register<RangeSlider, double>(
|
||||
nameof(TickFrequency));
|
||||
|
||||
public double TickFrequency
|
||||
{
|
||||
get => GetValue(TickFrequencyProperty);
|
||||
set => SetValue(TickFrequencyProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<AvaloniaList<double>?> TicksProperty =
|
||||
TickBar.TicksProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public AvaloniaList<double>? Ticks
|
||||
{
|
||||
get => GetValue(TicksProperty);
|
||||
set => SetValue(TicksProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
|
||||
Slider.TickPlacementProperty.AddOwner<RangeSlider>();
|
||||
|
||||
public TickPlacement TickPlacement
|
||||
{
|
||||
get => GetValue(TickPlacementProperty);
|
||||
set => SetValue(TickPlacementProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsSnapToTickProperty = AvaloniaProperty.Register<RangeSlider, bool>(
|
||||
nameof(IsSnapToTick));
|
||||
|
||||
public bool IsSnapToTick
|
||||
{
|
||||
get => GetValue(IsSnapToTickProperty);
|
||||
set => SetValue(IsSnapToTickProperty, value);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<RangeValueChangedEventArgs> ValueChangedEvent =
|
||||
RoutedEvent.Register<RangeSlider, RangeValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<RangeValueChangedEventArgs> ValueChanged
|
||||
{
|
||||
add => AddHandler(ValueChangedEvent, value);
|
||||
remove => RemoveHandler(ValueChangedEvent, value);
|
||||
}
|
||||
|
||||
static RangeSlider()
|
||||
{
|
||||
PressedMixin.Attach<RangeSlider>();
|
||||
FocusableProperty.OverrideDefaultValue<RangeSlider>(true);
|
||||
IsHitTestVisibleProperty.OverrideDefaultValue<RangeSlider>(true);
|
||||
OrientationProperty.OverrideDefaultValue<RangeSlider>(Orientation.Horizontal);
|
||||
OrientationProperty.Changed.AddClassHandler<RangeSlider, Orientation>((o,e)=>o.OnOrientationChanged(e));
|
||||
MinimumProperty.OverrideDefaultValue<RangeSlider>(0);
|
||||
MaximumProperty.OverrideDefaultValue<RangeSlider>(100);
|
||||
LowerValueProperty.OverrideDefaultValue<RangeSlider>(0);
|
||||
UpperValueProperty.OverrideDefaultValue<RangeSlider>(100);
|
||||
LowerValueProperty.Changed.AddClassHandler<RangeSlider, double>((o, e) => o.OnValueChanged(e, true));
|
||||
UpperValueProperty.Changed.AddClassHandler<RangeSlider, double>((o, e) => o.OnValueChanged(e, false));
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> args, bool isLower)
|
||||
{
|
||||
var oldValue = args.OldValue.Value;
|
||||
var newValue = args.NewValue.Value;
|
||||
if (Math.Abs(oldValue - newValue) > Tolerance)
|
||||
{
|
||||
RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower));
|
||||
}
|
||||
}
|
||||
|
||||
public RangeSlider()
|
||||
{
|
||||
UpdatePseudoClasses(Orientation);
|
||||
}
|
||||
|
||||
private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs<Orientation> args)
|
||||
{
|
||||
var value = args.NewValue.Value;
|
||||
UpdatePseudoClasses(value);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
_pointerMoveDisposable?.Dispose();
|
||||
_pointerPressedDisposable?.Dispose();
|
||||
_pointerReleasedDisposable?.Dispose();
|
||||
_track = e.NameScope.Find<RangeTrack>(PART_Track);
|
||||
_pointerMoveDisposable = this.AddDisposableHandler(PointerMovedEvent, PointerMove, RoutingStrategies.Tunnel);
|
||||
_pointerPressedDisposable = this.AddDisposableHandler(PointerPressedEvent, PointerPress, RoutingStrategies.Tunnel);
|
||||
_pointerReleasedDisposable = this.AddDisposableHandler(PointerReleasedEvent, PointerRelease, RoutingStrategies.Tunnel);
|
||||
}
|
||||
|
||||
private Thumb? _currentThumb;
|
||||
|
||||
private void PointerPress(object sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
var point = e.GetCurrentPoint(_track);
|
||||
_currentThumb = GetThumbByPoint(point);
|
||||
MoveToPoint(point);
|
||||
_isDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void PointerMove(object sender, PointerEventArgs args)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
_isDragging = false;
|
||||
return;
|
||||
}
|
||||
if (_isDragging)
|
||||
{
|
||||
MoveToPoint(args.GetCurrentPoint(_track));
|
||||
}
|
||||
}
|
||||
|
||||
private void PointerRelease(object sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
_isDragging = false;
|
||||
_currentThumb = null;
|
||||
}
|
||||
|
||||
private void MoveToPoint(PointerPoint posOnTrack)
|
||||
{
|
||||
if (_track is null) return;
|
||||
var value = GetValueByPoint(posOnTrack);
|
||||
var thumb = GetThumbByPoint(posOnTrack);
|
||||
if (_currentThumb !=null && _currentThumb != thumb) return;
|
||||
if (thumb is null) return;
|
||||
if (thumb == _track.LowerThumb)
|
||||
{
|
||||
SetCurrentValue(LowerValueProperty, IsSnapToTick ? SnapToTick(value) : value);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCurrentValue(UpperValueProperty, IsSnapToTick ? SnapToTick(value) : value);
|
||||
}
|
||||
}
|
||||
|
||||
private double SnapToTick(double value)
|
||||
{
|
||||
if (IsSnapToTick)
|
||||
{
|
||||
var previous = Minimum;
|
||||
var next = Maximum;
|
||||
|
||||
var ticks = Ticks;
|
||||
|
||||
if (ticks != null && ticks.Count > 0)
|
||||
{
|
||||
foreach (var tick in ticks)
|
||||
{
|
||||
if (MathUtilities.AreClose(tick, value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous))
|
||||
{
|
||||
previous = tick;
|
||||
}
|
||||
else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next))
|
||||
{
|
||||
next = tick;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (MathUtilities.GreaterThan(TickFrequency, 0.0))
|
||||
{
|
||||
previous = Minimum + Math.Round((value - Minimum) / TickFrequency) * TickFrequency;
|
||||
next = Math.Min(Maximum, previous + TickFrequency);
|
||||
}
|
||||
value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private Thumb? GetThumbByPoint(PointerPoint point)
|
||||
{
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var lowerThumbPosition = isHorizontal? _track?.LowerThumb?.Bounds.Center.X : _track?.LowerThumb?.Bounds.Center.Y;
|
||||
var upperThumbPosition = isHorizontal? _track?.UpperThumb?.Bounds.Center.X : _track?.UpperThumb?.Bounds.Center.Y;
|
||||
var pointerPosition = isHorizontal? point.Position.X : point.Position.Y;
|
||||
|
||||
var lowerDistance = Math.Abs((lowerThumbPosition ?? 0) - pointerPosition);
|
||||
var upperDistance = Math.Abs((upperThumbPosition ?? 0) - pointerPosition);
|
||||
|
||||
if (lowerDistance<upperDistance)
|
||||
{
|
||||
return _track?.LowerThumb;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _track?.UpperThumb;
|
||||
}
|
||||
}
|
||||
|
||||
private double GetValueByPoint(PointerPoint point)
|
||||
{
|
||||
if (_track is null) return 0;
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
|
||||
var pointPosition = isHorizontal ? point.Position.X : point.Position.Y;
|
||||
var ratio = _track.GetRatioByPoint(pointPosition);
|
||||
var range = Maximum - Minimum;
|
||||
var finalValue = ratio * range + Minimum;
|
||||
return finalValue;
|
||||
}
|
||||
|
||||
private void UpdatePseudoClasses(Orientation o)
|
||||
{
|
||||
this.PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical);
|
||||
this.PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal);
|
||||
}
|
||||
}
|
||||
506
src/Ursa/Controls/RangeSlider/RangeTrack.cs
Normal file
506
src/Ursa/Controls/RangeSlider/RangeTrack.cs
Normal file
@@ -0,0 +1,506 @@
|
||||
using System.Diagnostics;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Utilities;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 1. Notice that this is not used in ScrollBar, so ViewportSize related feature is not necessary.
|
||||
/// 2. Maximum, Minimum, MaxValue and MinValue are coerced there.
|
||||
/// </summary>
|
||||
[PseudoClasses(PC_Horizontal, PC_Vertical)]
|
||||
public class RangeTrack: Control
|
||||
{
|
||||
public const string PC_Horizontal = ":horizontal";
|
||||
public const string PC_Vertical = ":vertical";
|
||||
private double _density;
|
||||
private Vector _lastDrag;
|
||||
|
||||
private const double Tolerance = 0.0001;
|
||||
|
||||
public static readonly StyledProperty<double> MinimumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(Minimum), coerce: CoerceMinimum, defaultBindingMode:BindingMode.TwoWay);
|
||||
|
||||
public double Minimum
|
||||
{
|
||||
get => GetValue(MinimumProperty);
|
||||
set => SetValue(MinimumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> MaximumProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(Maximum), coerce: CoerceMaximum, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public double Maximum
|
||||
{
|
||||
get => GetValue(MaximumProperty);
|
||||
set => SetValue(MaximumProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> LowerValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(LowerValue), coerce: CoerceLowerValue, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public double LowerValue
|
||||
{
|
||||
get => GetValue(LowerValueProperty);
|
||||
set => SetValue(LowerValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<double> UpperValueProperty = AvaloniaProperty.Register<RangeTrack, double>(
|
||||
nameof(UpperValue), coerce: CoerceUpperValue, defaultBindingMode: BindingMode.TwoWay);
|
||||
|
||||
public double UpperValue
|
||||
{
|
||||
get => GetValue(UpperValueProperty);
|
||||
set => SetValue(UpperValueProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Orientation> OrientationProperty = AvaloniaProperty.Register<RangeTrack, Orientation>(
|
||||
nameof(Orientation));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> UpperSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(UpperSection));
|
||||
|
||||
public Control? UpperSection
|
||||
{
|
||||
get => GetValue(UpperSectionProperty);
|
||||
set => SetValue(UpperSectionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> LowerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(LowerSection));
|
||||
|
||||
public Control? LowerSection
|
||||
{
|
||||
get => GetValue(LowerSectionProperty);
|
||||
set => SetValue(LowerSectionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> InnerSectionProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(InnerSection));
|
||||
|
||||
public Control? InnerSection
|
||||
{
|
||||
get => GetValue(InnerSectionProperty);
|
||||
set => SetValue(InnerSectionProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Control?> TrackBackgroundProperty = AvaloniaProperty.Register<RangeTrack, Control?>(
|
||||
nameof(TrackBackground));
|
||||
|
||||
public Control? TrackBackground
|
||||
{
|
||||
get => GetValue(TrackBackgroundProperty);
|
||||
set => SetValue(TrackBackgroundProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Thumb?> UpperThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
||||
nameof(UpperThumb));
|
||||
|
||||
public Thumb? UpperThumb
|
||||
{
|
||||
get => GetValue(UpperThumbProperty);
|
||||
set => SetValue(UpperThumbProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Thumb?> LowerThumbProperty = AvaloniaProperty.Register<RangeTrack, Thumb?>(
|
||||
nameof(LowerThumb));
|
||||
|
||||
public Thumb? LowerThumb
|
||||
{
|
||||
get => GetValue(LowerThumbProperty);
|
||||
set => SetValue(LowerThumbProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> IsDirectionReversedProperty = AvaloniaProperty.Register<RangeTrack, bool>(
|
||||
nameof(IsDirectionReversed));
|
||||
|
||||
public bool IsDirectionReversed
|
||||
{
|
||||
get => GetValue(IsDirectionReversedProperty);
|
||||
set => SetValue(IsDirectionReversedProperty, value);
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent<RangeValueChangedEventArgs> ValueChangedEvent =
|
||||
RoutedEvent.Register<RangeTrack, RangeValueChangedEventArgs>(nameof(ValueChanged), RoutingStrategies.Bubble);
|
||||
|
||||
public event EventHandler<RangeValueChangedEventArgs> ValueChanged
|
||||
{
|
||||
add => AddHandler(ValueChangedEvent, value);
|
||||
remove => RemoveHandler(ValueChangedEvent, value);
|
||||
}
|
||||
|
||||
static RangeTrack()
|
||||
{
|
||||
OrientationProperty.Changed.AddClassHandler<RangeTrack, Orientation>((o, e) => o.OnOrientationChanged(e));
|
||||
LowerThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
||||
UpperThumbProperty.Changed.AddClassHandler<RangeTrack, Thumb?>((o, e) => o.OnThumbChanged(e));
|
||||
LowerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||
UpperSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||
InnerSectionProperty.Changed.AddClassHandler<RangeTrack, Control?>((o, e) => o.OnSectionChanged(e));
|
||||
MinimumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMinimumChanged(e));
|
||||
MaximumProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnMaximumChanged(e));
|
||||
LowerValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, true));
|
||||
UpperValueProperty.Changed.AddClassHandler<RangeTrack, double>((o, e) => o.OnValueChanged(e, false));
|
||||
AffectsArrange<RangeTrack>(
|
||||
MinimumProperty,
|
||||
MaximumProperty,
|
||||
LowerValueProperty,
|
||||
UpperValueProperty,
|
||||
OrientationProperty,
|
||||
IsDirectionReversedProperty);
|
||||
}
|
||||
|
||||
private void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> args, bool isLower)
|
||||
{
|
||||
var oldValue = args.OldValue.Value;
|
||||
var newValue = args.NewValue.Value;
|
||||
if (Math.Abs(oldValue - newValue) > Tolerance)
|
||||
{
|
||||
RaiseEvent(new RangeValueChangedEventArgs(ValueChangedEvent, this, oldValue, newValue, isLower));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMinimumChanged(AvaloniaPropertyChangedEventArgs<double> avaloniaPropertyChangedEventArgs)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
CoerceValue(MaximumProperty);
|
||||
CoerceValue(LowerValueProperty);
|
||||
CoerceValue(UpperValueProperty);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMaximumChanged(AvaloniaPropertyChangedEventArgs<double> avaloniaPropertyChangedEventArgs)
|
||||
{
|
||||
if (IsInitialized)
|
||||
{
|
||||
CoerceValue(LowerValueProperty);
|
||||
CoerceValue(UpperValueProperty);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSectionChanged(AvaloniaPropertyChangedEventArgs<Control?> args)
|
||||
{
|
||||
var oldSection = args.OldValue.Value;
|
||||
var newSection = args.NewValue.Value;
|
||||
if (oldSection is not null)
|
||||
{
|
||||
LogicalChildren.Remove(oldSection);
|
||||
VisualChildren.Remove(oldSection);
|
||||
}
|
||||
if (newSection is not null)
|
||||
{
|
||||
LogicalChildren.Add(newSection);
|
||||
VisualChildren.Add(newSection);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnThumbChanged(AvaloniaPropertyChangedEventArgs<Thumb?> args)
|
||||
{
|
||||
var oldThumb = args.OldValue.Value;
|
||||
var newThumb = args.NewValue.Value;
|
||||
if(oldThumb is not null)
|
||||
{
|
||||
LogicalChildren.Remove(oldThumb);
|
||||
VisualChildren.Remove(oldThumb);
|
||||
}
|
||||
if (newThumb is not null)
|
||||
{
|
||||
newThumb.ZIndex = 5;
|
||||
LogicalChildren.Add(newThumb);
|
||||
VisualChildren.Add(newThumb);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs<Orientation> args)
|
||||
{
|
||||
Orientation o = args.NewValue.Value;
|
||||
PseudoClasses.Set(PC_Horizontal, o == Orientation.Horizontal);
|
||||
PseudoClasses.Set(PC_Vertical, o == Orientation.Vertical);
|
||||
}
|
||||
|
||||
private static double CoerceMaximum(AvaloniaObject sender, double value)
|
||||
{
|
||||
return ValidateDouble(value)
|
||||
? Math.Max(value, sender.GetValue(MinimumProperty))
|
||||
: sender.GetValue(MaximumProperty);
|
||||
}
|
||||
|
||||
private static double CoerceMinimum(AvaloniaObject sender, double value)
|
||||
{
|
||||
return ValidateDouble(value) ? value : sender.GetValue(MinimumProperty);
|
||||
}
|
||||
|
||||
private static double CoerceLowerValue(AvaloniaObject sender, double value)
|
||||
{
|
||||
if (!ValidateDouble(value)) return sender.GetValue(LowerValueProperty);
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty));
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(UpperValueProperty));
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double CoerceUpperValue(AvaloniaObject sender, double value)
|
||||
{
|
||||
if (!ValidateDouble(value)) return sender.GetValue(UpperValueProperty);
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(MinimumProperty), sender.GetValue(MaximumProperty));
|
||||
value = MathUtilities.Clamp(value, sender.GetValue(LowerValueProperty), sender.GetValue(MaximumProperty));
|
||||
return value;
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
CoerceValue(MaximumProperty);
|
||||
CoerceValue(LowerValueProperty);
|
||||
CoerceValue(UpperValueProperty);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var desiredSize = new Size();
|
||||
if (LowerThumb is not null && UpperThumb is not null)
|
||||
{
|
||||
LowerThumb.Measure(availableSize);
|
||||
UpperThumb.Measure(availableSize);
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
desiredSize = new Size(LowerThumb.DesiredSize.Width + UpperThumb.DesiredSize.Width,
|
||||
Math.Max(LowerThumb.DesiredSize.Height, UpperThumb.DesiredSize.Height));
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredSize = new Size(Math.Max(LowerThumb.DesiredSize.Width, UpperThumb.DesiredSize.Width),
|
||||
LowerThumb.DesiredSize.Height + UpperThumb.DesiredSize.Height);
|
||||
}
|
||||
}
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
var vertical = Orientation == Orientation.Vertical;
|
||||
double lowerButtonLength, innerButtonLength, upperButtonLength, lowerThumbLength, upperThumbLength;
|
||||
ComputeSliderLengths(finalSize, vertical, out lowerButtonLength, out innerButtonLength, out upperButtonLength,
|
||||
out lowerThumbLength, out upperThumbLength);
|
||||
var offset = new Point();
|
||||
var pieceSize = finalSize;
|
||||
if (vertical)
|
||||
{
|
||||
CoerceLength(ref lowerButtonLength, finalSize.Height);
|
||||
CoerceLength(ref innerButtonLength, finalSize.Height);
|
||||
CoerceLength(ref upperButtonLength, finalSize.Height);
|
||||
CoerceLength(ref lowerThumbLength, finalSize.Height);
|
||||
CoerceLength(ref upperThumbLength, finalSize.Height);
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
offset = offset.WithY(lowerThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithHeight(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + lowerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(lowerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(lowerButtonLength + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = offset.WithY(upperThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithHeight(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + upperButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithY(offset.Y + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(upperButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithY(upperButtonLength + innerButtonLength);
|
||||
pieceSize = pieceSize.WithHeight(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
CoerceLength(ref lowerButtonLength, finalSize.Width);
|
||||
CoerceLength(ref innerButtonLength, finalSize.Width);
|
||||
CoerceLength(ref upperButtonLength, finalSize.Width);
|
||||
CoerceLength(ref lowerThumbLength, finalSize.Width);
|
||||
CoerceLength(ref upperThumbLength, finalSize.Width);
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
offset = offset.WithX(upperThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithWidth(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + upperButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(upperButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(upperButtonLength+innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
offset = offset.WithX(lowerThumbLength * 0.5);
|
||||
pieceSize = pieceSize.WithWidth(lowerButtonLength);
|
||||
LowerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + lowerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(innerButtonLength);
|
||||
InnerSection?.Arrange(new Rect(offset, pieceSize));
|
||||
offset = offset.WithX(offset.X + innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(upperButtonLength);
|
||||
UpperSection?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(lowerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(lowerThumbLength);
|
||||
LowerThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
offset = offset.WithX(lowerButtonLength + innerButtonLength);
|
||||
pieceSize = pieceSize.WithWidth(upperThumbLength);
|
||||
UpperThumb?.Arrange(new Rect(offset, pieceSize));
|
||||
|
||||
}
|
||||
}
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
private void ComputeSliderLengths(
|
||||
Size arrangeSize,
|
||||
bool isVertical,
|
||||
out double lowerButtonLength,
|
||||
out double innerButtonLength,
|
||||
out double upperButtonLength,
|
||||
out double lowerThumbLength,
|
||||
out double upperThumbLength)
|
||||
{
|
||||
double range = Math.Max(0, Maximum - Minimum);
|
||||
range += double.Epsilon;
|
||||
double lowerOffset = Math.Min(range, LowerValue - Minimum);
|
||||
double upperOffset = Math.Min(range, UpperValue - Minimum);
|
||||
|
||||
double trackLength;
|
||||
if (isVertical)
|
||||
{
|
||||
trackLength = arrangeSize.Height;
|
||||
lowerThumbLength = LowerThumb?.DesiredSize.Height ?? 0;
|
||||
upperThumbLength = UpperThumb?.DesiredSize.Height ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
trackLength = arrangeSize.Width;
|
||||
lowerThumbLength = LowerThumb?.DesiredSize.Width ?? 0;
|
||||
upperThumbLength = UpperThumb?.DesiredSize.Width ?? 0;
|
||||
}
|
||||
|
||||
CoerceLength(ref lowerThumbLength, trackLength);
|
||||
CoerceLength(ref upperThumbLength, trackLength);
|
||||
|
||||
double remainingLength = trackLength - lowerThumbLength * 0.5 - upperThumbLength * 0.5;
|
||||
|
||||
lowerButtonLength = remainingLength * lowerOffset / range;
|
||||
upperButtonLength = remainingLength * (range-upperOffset) / range;
|
||||
innerButtonLength = remainingLength - lowerButtonLength - upperButtonLength;
|
||||
|
||||
_density = range / remainingLength;
|
||||
}
|
||||
|
||||
private static void CoerceLength(ref double componentLength, double trackLength)
|
||||
{
|
||||
if (componentLength < 0)
|
||||
{
|
||||
componentLength = 0.0;
|
||||
}
|
||||
else if (componentLength > trackLength || double.IsNaN(componentLength))
|
||||
{
|
||||
componentLength = trackLength;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ValidateDouble(double value)
|
||||
{
|
||||
return !double.IsInfinity(value) && !double.IsNaN(value);
|
||||
}
|
||||
|
||||
internal double GetRatioByPoint(double position)
|
||||
{
|
||||
bool isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var range = isHorizontal?
|
||||
LowerSection?.Bounds.Width + InnerSection?.Bounds.Width + UpperSection?.Bounds.Width ?? double.Epsilon
|
||||
: LowerSection?.Bounds.Height + InnerSection?.Bounds.Height + UpperSection?.Bounds.Height ?? double.Epsilon;
|
||||
if (isHorizontal)
|
||||
{
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
double trackStart = UpperThumb?.Bounds.Width/2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 1.0;
|
||||
if (position > trackEnd) return 0.0;
|
||||
double diff = trackEnd - position;
|
||||
return diff / range;
|
||||
}
|
||||
else
|
||||
{
|
||||
double trackStart = LowerThumb?.Bounds.Width/2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 0.0;
|
||||
if (position > trackEnd) return 1.0;
|
||||
double diff = position - trackStart;
|
||||
return diff / range;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsDirectionReversed)
|
||||
{
|
||||
double trackStart = LowerThumb?.Bounds.Height / 2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 0.0;
|
||||
if (position > trackEnd) return 1.0;
|
||||
double diff = position - trackStart;
|
||||
return diff / range;
|
||||
}
|
||||
else
|
||||
{
|
||||
double trackStart = UpperThumb?.Bounds.Height / 2 ?? 0;
|
||||
double trackEnd = trackStart + range;
|
||||
if (position < trackStart) return 1.0;
|
||||
if (position > trackEnd) return 0.0;
|
||||
double diff = trackEnd - position;
|
||||
return diff / range;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs
Normal file
34
src/Ursa/Controls/RangeSlider/RangeValueChangedEventArgs.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class RangeValueChangedEventArgs: RoutedEventArgs
|
||||
{
|
||||
public double OldValue { get; set; }
|
||||
public double NewValue { get; set; }
|
||||
public bool IsLower { get; set; }
|
||||
|
||||
public RangeValueChangedEventArgs(
|
||||
RoutedEvent routedEvent,
|
||||
object source,
|
||||
double oldValue,
|
||||
double newValue,
|
||||
bool isLower = true) : base(routedEvent, source)
|
||||
{
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
IsLower = isLower;
|
||||
}
|
||||
|
||||
public RangeValueChangedEventArgs(
|
||||
RoutedEvent routedEvent,
|
||||
double oldValue,
|
||||
double newValue,
|
||||
bool isLower = true) : base(routedEvent)
|
||||
{
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
IsLower = isLower;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user