using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Events;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using RGB.NET.Core;
using DryIoc;
using Color = RGB.NET.Core.Color;
using Point = Avalonia.Point;
using Size = Avalonia.Size;
namespace Artemis.UI.Shared;
///
/// Visualizes an with optional per-LED colors
///
public class DeviceVisualizer : Control
{
private readonly ICoreService _coreService;
private readonly List _deviceVisualizerLeds;
private Rect _deviceBounds;
private RenderTargetBitmap? _deviceImage;
private ArtemisDevice? _oldDevice;
private bool _loading;
private Color[] _previousState = Array.Empty();
///
public DeviceVisualizer()
{
_coreService = UI.Locator.Resolve();
_deviceVisualizerLeds = new List();
PointerReleased += OnPointerReleased;
PropertyChanged += OnPropertyChanged;
}
///
public override void Render(DrawingContext drawingContext)
{
if (Device == null || _deviceBounds.Width == 0 || _deviceBounds.Height == 0 || _loading)
return;
// Determine the scale required to fit the desired size of the control
double scale = Math.Min(Bounds.Width / _deviceBounds.Width, Bounds.Height / _deviceBounds.Height);
DrawingContext.PushedState? boundsPush = null;
try
{
// Scale the visualization in the desired bounding box
if (Bounds.Width > 0 && Bounds.Height > 0)
boundsPush = drawingContext.PushTransform(Matrix.CreateScale(scale, scale));
// Apply device rotation
using DrawingContext.PushedState translationPush = drawingContext.PushTransform(Matrix.CreateTranslation(0 - _deviceBounds.Left, 0 - _deviceBounds.Top));
using DrawingContext.PushedState rotationPush = drawingContext.PushTransform(Matrix.CreateRotation(Matrix.ToRadians(Device.Rotation)));
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(
_deviceImage,
new Rect(_deviceImage.Size),
new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height)
);
if (!ShowColors)
return;
lock (_deviceVisualizerLeds)
{
// Apply device scale
using DrawingContext.PushedState scalePush = drawingContext.PushTransform(Matrix.CreateScale(Device.Scale, Device.Scale));
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.RenderGeometry(drawingContext);
}
}
finally
{
boundsPush?.Dispose();
}
}
///
/// Occurs when a LED of the device has been clicked
///
public event EventHandler? LedClicked;
///
/// Occurs when the device was clicked but not on a LED.
///
public event EventHandler? Clicked;
///
/// Invokes the event
///
protected virtual void OnLedClicked(LedClickedEventArgs e)
{
LedClicked?.Invoke(this, e);
}
///
/// Invokes the event
///
protected virtual void OnClicked(PointerReleasedEventArgs e)
{
Clicked?.Invoke(this, e);
}
private bool IsDirty()
{
if (Device == null)
return false;
bool difference = false;
int newLedCount = Device.RgbDevice.Count();
if (_previousState.Length != newLedCount)
{
_previousState = new Color[newLedCount];
difference = true;
}
// Check all LEDs for differences and copy the colors to a new state
int index = 0;
foreach (Led led in Device.RgbDevice)
{
if (_previousState[index] != led.Color)
difference = true;
_previousState[index] = led.Color;
index++;
}
return difference;
}
private void Update()
{
InvalidateVisual();
}
private Rect MeasureDevice()
{
if (Device == null)
return new Rect();
Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
Geometry geometry = new RectangleGeometry(deviceRect);
geometry.Transform = new RotateTransform(Device.Rotation);
return geometry.Bounds;
}
private void OnFrameRendered(object? sender, FrameRenderedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
if (ShowColors && IsVisible && Opacity > 0 && IsDirty())
Update();
}, DispatcherPriority.Background);
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (Device == null)
return;
Point position = e.GetPosition(this);
double x = position.X / Bounds.Width;
double y = position.Y / Bounds.Height;
Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height);
DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.HitTest(scaledPosition));
if (deviceVisualizerLed != null)
OnLedClicked(new LedClickedEventArgs(deviceVisualizerLed.Led.Device, deviceVisualizerLed.Led, e));
else
OnClicked(e);
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == DeviceProperty)
SetupForDevice();
}
private void DevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
private void DeviceUpdated(object? sender, EventArgs e)
{
Dispatcher.UIThread.Post(SetupForDevice, DispatcherPriority.Background);
}
#region Properties
///
/// Gets or sets the to display
///
public static readonly StyledProperty DeviceProperty =
AvaloniaProperty.Register(nameof(Device));
///
/// Gets or sets the to display
///
public ArtemisDevice? Device
{
get => GetValue(DeviceProperty);
set => SetValue(DeviceProperty, value);
}
///
/// Gets or sets boolean indicating whether or not to show per-LED colors
///
public static readonly StyledProperty ShowColorsProperty =
AvaloniaProperty.Register(nameof(ShowColors));
///
/// Gets or sets a boolean indicating whether or not to show per-LED colors
///
public bool ShowColors
{
get => GetValue(ShowColorsProperty);
set => SetValue(ShowColorsProperty, value);
}
#endregion
#region Lifetime management
///
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_deviceImage?.Dispose();
_deviceImage = null;
if (Device != null)
{
Device.RgbDevice.PropertyChanged -= DevicePropertyChanged;
Device.DeviceUpdated -= DeviceUpdated;
}
base.OnDetachedFromVisualTree(e);
}
///
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_coreService.FrameRendered += OnFrameRendered;
base.OnAttachedToLogicalTree(e);
}
///
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_coreService.FrameRendered -= OnFrameRendered;
base.OnDetachedFromLogicalTree(e);
}
private void SetupForDevice()
{
lock (_deviceVisualizerLeds)
{
_deviceVisualizerLeds.Clear();
}
if (_oldDevice != null)
{
_oldDevice.RgbDevice.PropertyChanged -= DevicePropertyChanged;
_oldDevice.DeviceUpdated -= DeviceUpdated;
}
_oldDevice = Device;
if (Device == null)
return;
_deviceBounds = MeasureDevice();
_loading = true;
Device.RgbDevice.PropertyChanged += DevicePropertyChanged;
Device.DeviceUpdated += DeviceUpdated;
// Create all the LEDs
lock (_deviceVisualizerLeds)
{
foreach (ArtemisLed artemisLed in Device.Leds)
_deviceVisualizerLeds.Add(new DeviceVisualizerLed(artemisLed));
}
// Load the device main image on a background thread
ArtemisDevice? device = Device;
Dispatcher.UIThread.Post(() =>
{
try
{
if (device.Layout?.Image == null || !File.Exists(device.Layout.Image.LocalPath))
{
_deviceImage?.Dispose();
_deviceImage = null;
return;
}
// Create a bitmap that'll be used to render the device and LED images just once
// Render 4 times the actual size of the device to make sure things look sharp when zoomed in
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.ActualSize.Width * 2, (int) device.RgbDevice.ActualSize.Height * 2));
using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
using Bitmap bitmap = new(device.Layout.Image.LocalPath);
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize);
context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size));
lock (_deviceVisualizerLeds)
{
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.DrawBitmap(context, 2 * Device.Scale);
}
_deviceImage?.Dispose();
_deviceImage = renderTargetBitmap;
InvalidateMeasure();
}
catch (Exception)
{
// ignored
}
finally
{
_loading = false;
}
});
}
///
protected override Size MeasureOverride(Size availableSize)
{
if (_deviceBounds.Width <= 0 || _deviceBounds.Height <= 0)
return new Size(0, 0);
double availableWidth = double.IsInfinity(availableSize.Width) ? _deviceBounds.Width : availableSize.Width;
double availableHeight = double.IsInfinity(availableSize.Height) ? _deviceBounds.Height : availableSize.Height;
double bestRatio = Math.Min(availableWidth / _deviceBounds.Width, availableHeight / _deviceBounds.Height);
return new Size(_deviceBounds.Width * bestRatio, _deviceBounds.Height * bestRatio);
}
#endregion
}