using System; using Avalonia; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; using FluentAvalonia.Core; using FluentAvalonia.UI.Controls; namespace Artemis.UI.Shared.Controls; /// /// Represents a number box that can be mutated by dragging over it horizontally /// public class DraggableNumberBox : UserControl { /// /// Defines the property. /// public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register(nameof(Value), defaultBindingMode: BindingMode.TwoWay, notifying: ValueChanged); /// /// Defines the property. /// public static readonly StyledProperty MinimumProperty = AvaloniaProperty.Register(nameof(Minimum), double.MinValue); /// /// Defines the property. /// public static readonly StyledProperty MaximumProperty = AvaloniaProperty.Register(nameof(Maximum), double.MaxValue); /// /// Defines the property. /// public static readonly StyledProperty LargeChangeProperty = AvaloniaProperty.Register(nameof(LargeChange)); /// /// Defines the property. /// public static readonly StyledProperty SmallChangeProperty = AvaloniaProperty.Register(nameof(SmallChange)); /// /// Defines the property. /// public static readonly StyledProperty SimpleNumberFormatProperty = AvaloniaProperty.Register(nameof(SimpleNumberFormat)); /// /// Defines the property. /// public static readonly StyledProperty PrefixProperty = AvaloniaProperty.Register(nameof(Prefix)); /// /// Defines the property. /// public static readonly StyledProperty SuffixProperty = AvaloniaProperty.Register(nameof(Suffix)); private readonly NumberBox _numberBox; private TextBox? _inputTextBox; private double _lastX; private bool _moved; private double _startX; private bool _updating; /// /// Creates a new instance of the class. /// public DraggableNumberBox() { InitializeComponent(); _numberBox = this.Get("NumberBox"); _numberBox.Value = Value; PointerPressed += OnPointerPressed; PointerMoved += OnPointerMoved; PointerReleased += OnPointerReleased; AddHandler(KeyUpEvent, HandleKeyUp, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); } /// /// Gets or sets the value of the number box. /// public double Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); } /// /// Gets or sets the minimum of the number box. /// public double Minimum { get => GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); } /// /// Gets or sets the maximum of the number box. /// public double Maximum { get => GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); } /// /// Gets or sets the amount with which to increase/decrease the value when dragging. /// public double LargeChange { get => GetValue(LargeChangeProperty); set => SetValue(LargeChangeProperty, value); } /// /// Gets or sets the amount with which to increase/decrease the value when dragging and holding down shift. /// public double SmallChange { get => GetValue(SmallChangeProperty); set => SetValue(SmallChangeProperty, value); } /// /// Gets or sets the number format string used to format the value into a display value. /// public string SimpleNumberFormat { get => GetValue(SimpleNumberFormatProperty); set => SetValue(SimpleNumberFormatProperty, value); } /// /// Gets or sets the prefix to show before the value. /// public string? Prefix { get => GetValue(PrefixProperty); set => SetValue(PrefixProperty, value); } /// /// Gets or sets the affix to show behind the value. /// public string? Suffix { get => GetValue(SuffixProperty); set => SetValue(SuffixProperty, value); } /// /// Occurs when the user starts dragging over the control. /// public event TypedEventHandler? DragStarted; /// /// Occurs when the user finishes dragging over the control. /// public event TypedEventHandler? DragFinished; private static void ValueChanged(IAvaloniaObject sender, bool before) { if (before) return; DraggableNumberBox draggable = (DraggableNumberBox) sender; if (!(Math.Abs(draggable._numberBox.Value - draggable.Value) > 0.00001)) return; draggable._updating = true; draggable._numberBox.Value = draggable.Value; draggable._updating = false; } private void HandleKeyUp(object? sender, KeyEventArgs e) { if (e.Key == Key.Enter || e.Key == Key.Escape) Parent?.Focus(); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { PointerPoint point = e.GetCurrentPoint(this); _inputTextBox = _numberBox.FindDescendantOfType(); _moved = false; _startX = point.Position.X; _lastX = point.Position.X; e.Handled = true; } private void OnPointerMoved(object? sender, PointerEventArgs e) { PointerPoint point = e.GetCurrentPoint(this); if (!point.Properties.IsLeftButtonPressed || _inputTextBox == null || _inputTextBox.IsFocused) return; if (!_moved && Math.Abs(point.Position.X - _startX) < 2) { _lastX = point.Position.X; return; } if (!_moved) { // Let our parent take focus, it would make more sense to take focus ourselves but that hides the collider Parent?.Focus(); _moved = true; e.Pointer.Capture(this); DragStarted?.Invoke(this, EventArgs.Empty); } double smallChange; if (SmallChange != 0) smallChange = SmallChange; else if (LargeChange != 0) smallChange = LargeChange / 10; else smallChange = 0.1; double largeChange; if (LargeChange != 0) largeChange = LargeChange; else if (LargeChange != 0) largeChange = LargeChange * 10; else largeChange = 1; double changeMultiplier = e.KeyModifiers.HasFlag(KeyModifiers.Shift) ? smallChange : largeChange; Value = Math.Clamp(Value + (point.Position.X - _lastX) * changeMultiplier, Minimum, Maximum); _lastX = point.Position.X; e.Handled = true; } private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) { if (!_moved) { _inputTextBox?.Focus(); } else { _moved = false; DragFinished?.Invoke(this, EventArgs.Empty); } e.Handled = true; } private void NumberBox_OnValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) { if (_updating) return; if (args.NewValue < Minimum) { _numberBox.Value = Minimum; return; } if (args.NewValue > Maximum) { _numberBox.Value = Maximum; return; } if (Math.Abs(Value - _numberBox.Value) > 0.00001) Value = _numberBox.Value; } }