using System; using Artemis.Core; using Artemis.UI.Shared.Events; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; namespace Artemis.UI.Shared; /// /// Visualizes an with optional per-LED colors /// public class SelectionRectangle : Control { /// /// Defines the property. /// public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), new SolidColorBrush(Colors.CadetBlue, 0.25)); /// /// Defines the property. /// public static readonly StyledProperty BorderBrushProperty = AvaloniaProperty.Register(nameof(BorderBrush), new SolidColorBrush(Colors.CadetBlue)); /// /// Defines the property. /// public static readonly StyledProperty BorderThicknessProperty = AvaloniaProperty.Register(nameof(BorderThickness), 1); /// /// Defines the property. /// public static readonly StyledProperty BorderRadiusProperty = AvaloniaProperty.Register(nameof(BorderRadius)); /// /// Defines the property. /// public static readonly StyledProperty InputElementProperty = AvaloniaProperty.Register(nameof(InputElement), notifying: OnInputElementChanged); /// /// Defines the property. /// public static readonly StyledProperty ZoomRatioProperty = AvaloniaProperty.Register(nameof(ZoomRatio), 1); /// /// Defines the read-only property. /// public static readonly DirectProperty IsSelectingProperty = AvaloniaProperty.RegisterDirect(nameof(IsSelecting), o => o.IsSelecting); private Rect? _absoluteRect; private Point _absoluteStartPosition; private Rect? _displayRect; private bool _isSelecting; private IControl? _oldInputElement; private Point _startPosition; private Point _lastPosition; /// public SelectionRectangle() { AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty); IsHitTestVisible = false; } /// /// Gets or sets a brush used to paint the control's background. /// public IBrush Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } /// /// Gets or sets a brush used to paint the control's border /// public IBrush BorderBrush { get => GetValue(BorderBrushProperty); set => SetValue(BorderBrushProperty, value); } /// /// Gets or sets the width of the control's border /// public double BorderThickness { get => GetValue(BorderThicknessProperty); set => SetValue(BorderThicknessProperty, value); } /// /// Gets or sets the radius of the control's border /// public double BorderRadius { get => GetValue(BorderRadiusProperty); set => SetValue(BorderRadiusProperty, value); } /// /// Gets or sets the element that captures input for the selection rectangle. /// public IControl? InputElement { get => GetValue(InputElementProperty); set => SetValue(InputElementProperty, value); } /// /// Gets or sets the zoom ratio to counteract when drawing /// public double ZoomRatio { get => GetValue(ZoomRatioProperty); set => SetValue(ZoomRatioProperty, value); } /// /// Gets a boolean indicating whether the selection rectangle is currently performing a selection. /// public bool IsSelecting { get => _isSelecting; private set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value); } /// /// Occurs when the selection rect is being updated, indicating the user is dragging. /// public event EventHandler? SelectionUpdated; /// /// Occurs when the selection has finished, indicating the user stopped dragging. /// public event EventHandler? SelectionFinished; /// /// Invokes the event /// /// protected virtual void OnSelectionUpdated(SelectionRectangleEventArgs e) { SelectionUpdated?.Invoke(this, e); } /// /// Invokes the event /// /// protected virtual void OnSelectionFinished(SelectionRectangleEventArgs e) { SelectionFinished?.Invoke(this, e); } private static void OnInputElementChanged(IAvaloniaObject sender, bool before) { ((SelectionRectangle) sender).SubscribeToInputElement(); } private void ParentOnPointerMoved(object? sender, PointerEventArgs e) { // Point moved seems to trigger when the element under the mouse changes? // I'm not sure why this is needed but this check makes sure the position really hasn't changed. Point position = e.GetCurrentPoint(null).Position; if (position == _lastPosition) return; _lastPosition = position; if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; // Capture the pointer and initialize dragging the first time it moves if (!ReferenceEquals(e.Pointer.Captured, this)) { e.Pointer.Capture(this); _startPosition = e.GetPosition(Parent); _absoluteStartPosition = e.GetPosition(VisualRoot); _displayRect = null; } Point currentPosition = e.GetPosition(Parent); Point absoluteCurrentPosition = e.GetPosition(VisualRoot); _displayRect = new Rect( new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)), new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y)) ); _absoluteRect = new Rect( new Point(Math.Min(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Min(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)), new Point(Math.Max(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Max(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)) ); OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); InvalidateVisual(); IsSelecting = true; } private void ParentOnPointerReleased(object? sender, PointerReleasedEventArgs e) { if (!ReferenceEquals(e.Pointer.Captured, this)) return; e.Pointer.Capture(null); if (_displayRect != null && _absoluteRect != null) { OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers)); e.Handled = true; } _displayRect = null; InvalidateVisual(); IsSelecting = false; } private void SubscribeToInputElement() { if (_oldInputElement != null) { _oldInputElement.PointerMoved -= ParentOnPointerMoved; _oldInputElement.PointerReleased -= ParentOnPointerReleased; } _oldInputElement = InputElement; if (InputElement != null) { InputElement.PointerMoved += ParentOnPointerMoved; InputElement.PointerReleased += ParentOnPointerReleased; } } #region Overrides of Visual /// public override void Render(DrawingContext drawingContext) { if (_displayRect != null) drawingContext.DrawRectangle(Background, new Pen(BorderBrush, BorderThickness / ZoomRatio), _displayRect.Value, BorderRadius / ZoomRatio, BorderRadius / ZoomRatio); } /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { SubscribeToInputElement(); base.OnAttachedToVisualTree(e); } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { if (_oldInputElement != null) { _oldInputElement.PointerMoved -= ParentOnPointerMoved; _oldInputElement.PointerReleased -= ParentOnPointerReleased; _oldInputElement = null; } base.OnDetachedFromVisualTree(e); } #endregion }