diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs
index c621eb5c7..589b7f5ba 100644
--- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs
+++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs
@@ -11,7 +11,7 @@ namespace Artemis.Core
///
/// A gradient containing a list of s
///
- public class ColorGradient : IList, INotifyCollectionChanged
+ public class ColorGradient : IList, IList, INotifyCollectionChanged
{
private static readonly SKColor[] FastLedRainbow =
{
@@ -173,13 +173,25 @@ namespace Artemis.Core
internal void Sort()
{
- _stops.Sort((a, b) => a.Position.CompareTo(b.Position));
+ int requiredIndex = 0;
+ foreach (ColorGradientStop colorGradientStop in _stops.OrderBy(s => s.Position).ToList())
+ {
+ int actualIndex = _stops.IndexOf(colorGradientStop);
+ if (requiredIndex != actualIndex)
+ {
+ _stops.RemoveAt(actualIndex);
+ _stops.Insert(requiredIndex, colorGradientStop);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, colorGradientStop, requiredIndex, actualIndex));
+ }
+
+ requiredIndex++;
+ }
}
private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Sort();
- OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ OnStopChanged();
}
#region Implementation of IEnumerable
@@ -209,8 +221,16 @@ namespace Artemis.Core
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _stops.IndexOf(item)));
}
-
///
+ public int Add(object? value)
+ {
+ if (value is ColorGradientStop stop)
+ _stops.Add(stop);
+
+ return IndexOf(value);
+ }
+
+ ///
public void Clear()
{
foreach (ColorGradientStop item in _stops)
@@ -219,6 +239,32 @@ namespace Artemis.Core
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
+ ///
+ public bool Contains(object? value)
+ {
+ return _stops.Contains(value);
+ }
+
+ ///
+ public int IndexOf(object? value)
+ {
+ return _stops.IndexOf(value);
+ }
+
+ ///
+ public void Insert(int index, object? value)
+ {
+ if (value is ColorGradientStop stop)
+ _stops.Insert(index, stop);
+ }
+
+ ///
+ public void Remove(object? value)
+ {
+ if (value is ColorGradientStop stop)
+ _stops.Remove(stop);
+ }
+
///
public bool Contains(ColorGradientStop item)
{
@@ -244,11 +290,29 @@ namespace Artemis.Core
}
///
+ public void CopyTo(Array array, int index)
+ {
+ _stops.CopyTo((ColorGradientStop[]) array, index);
+ }
+
+ ///
public int Count => _stops.Count;
///
+ public bool IsSynchronized => false;
+
+ ///
+ public object SyncRoot => this;
+
+ ///
public bool IsReadOnly => false;
+ object? IList.this[int index]
+ {
+ get => this[index];
+ set => this[index] = (ColorGradientStop) value!;
+ }
+
#endregion
#region Implementation of IList
@@ -268,7 +332,7 @@ namespace Artemis.Core
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item)));
}
- ///
+ ///
public void RemoveAt(int index)
{
_stops[index].PropertyChanged -= ItemOnPropertyChanged;
@@ -276,6 +340,8 @@ namespace Artemis.Core
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index));
}
+ public bool IsFixedSize { get; }
+
///
public ColorGradientStop this[int index]
{
@@ -304,5 +370,15 @@ namespace Artemis.Core
}
#endregion
+
+ ///
+ /// Occurs when any of the stops has changed in some way
+ ///
+ public event EventHandler? StopChanged;
+
+ private void OnStopChanged()
+ {
+ StopChanged?.Invoke(this, EventArgs.Empty);
+ }
}
}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs
new file mode 100644
index 000000000..8359568f5
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Specialized;
+using System.Linq;
+using Artemis.Core;
+using Avalonia;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Media;
+
+namespace Artemis.UI.Shared.Controls.GradientPicker;
+
+public class GradientPicker : TemplatedControl
+{
+ private LinearGradientBrush _linearGradientBrush = new();
+
+ ///
+ /// Gets or sets the color gradient.
+ ///
+ public static readonly StyledProperty ColorGradientProperty =
+ AvaloniaProperty.Register(nameof(Core.ColorGradient), notifying: ColorGradientChanged, defaultValue: ColorGradient.GetUnicornBarf());
+
+ ///
+ /// Gets or sets the currently selected color stop.
+ ///
+ public static readonly StyledProperty SelectedColorStopProperty =
+ AvaloniaProperty.Register(nameof(SelectedColorStop), defaultBindingMode: BindingMode.TwoWay);
+
+ ///
+ /// Gets the linear gradient brush representing the color gradient.
+ ///
+ public static readonly DirectProperty LinearGradientBrushProperty =
+ AvaloniaProperty.RegisterDirect(nameof(LinearGradientBrush), g => g.LinearGradientBrush);
+
+ ///
+ /// Gets or sets the color gradient.
+ ///
+ public ColorGradient ColorGradient
+ {
+ get => GetValue(ColorGradientProperty);
+ set => SetValue(ColorGradientProperty, value);
+ }
+
+ ///
+ /// Gets or sets the currently selected color stop.
+ ///
+ public ColorGradientStop? SelectedColorStop
+ {
+ get => GetValue(SelectedColorStopProperty);
+ set => SetValue(SelectedColorStopProperty, value);
+ }
+
+ ///
+ /// Gets the linear gradient brush representing the color gradient.
+ ///
+ public LinearGradientBrush LinearGradientBrush
+ {
+ get => _linearGradientBrush;
+ private set => SetAndRaise(LinearGradientBrushProperty, ref _linearGradientBrush, value);
+ }
+
+ private static void ColorGradientChanged(IAvaloniaObject sender, bool before)
+ {
+ if (before)
+ (sender as GradientPicker)?.Unsubscribe();
+ else
+ (sender as GradientPicker)?.Subscribe();
+ }
+
+ private void Subscribe()
+ {
+ ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
+ ColorGradient.StopChanged += ColorGradientOnStopChanged;
+
+ UpdateGradient();
+ SelectedColorStop = ColorGradient.FirstOrDefault();
+ }
+
+ private void Unsubscribe()
+ {
+ ColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
+ ColorGradient.StopChanged -= ColorGradientOnStopChanged;
+ }
+
+ private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ UpdateGradient();
+ }
+
+ private void ColorGradientOnStopChanged(object? sender, EventArgs e)
+ {
+ UpdateGradient();
+ }
+
+ private void UpdateGradient()
+ {
+ // Remove old stops
+
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalse
+ if (ColorGradient == null)
+ return;
+
+ // Add new stops
+
+ // Update the display gradient
+ GradientStops collection = new();
+ foreach (ColorGradientStop c in ColorGradient.OrderBy(s => s.Position))
+ collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
+ LinearGradientBrush = new LinearGradientBrush {GradientStops = collection};
+ }
+
+ private void SelectColorStop(object? sender, PointerReleasedEventArgs e)
+ {
+ if (sender is IDataContextProvider dataContextProvider && dataContextProvider.DataContext is ColorGradientStop colorStop)
+ SelectedColorStop = colorStop;
+ }
+
+ #region Overrides of Visual
+
+ ///
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ Subscribe();
+ base.OnAttachedToVisualTree(e);
+ }
+
+ ///
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ Unsubscribe();
+ base.OnDetachedFromVisualTree(e);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs
new file mode 100644
index 000000000..d4872c39a
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Controls/GradientPicker/GradientPickerColorStop.cs
@@ -0,0 +1,199 @@
+using System;
+using Artemis.Core;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+
+namespace Artemis.UI.Shared.Controls.GradientPicker;
+
+public class GradientPickerColorStop : TemplatedControl
+{
+ private static ColorGradientStop? _draggingStop;
+ private static IPointer? _dragPointer;
+
+ ///
+ /// Gets or sets the gradient picker.
+ ///
+ public static readonly StyledProperty GradientPickerProperty =
+ AvaloniaProperty.Register(nameof(GradientPicker), notifying: Notifying);
+
+ private static void Notifying(IAvaloniaObject sender, bool before)
+ {
+ if (sender is not GradientPickerColorStop self)
+ return;
+
+ if (before && self.GradientPicker != null)
+ self.GradientPicker.PropertyChanged -= self.GradientPickerOnPropertyChanged;
+ else if (self.GradientPicker != null)
+ self.GradientPicker.PropertyChanged += self.GradientPickerOnPropertyChanged;
+
+ self.IsSelected = self.GradientPicker?.SelectedColorStop == self.ColorStop;
+ }
+
+ ///
+ /// Gets or sets the color stop.
+ ///
+ public static readonly StyledProperty ColorStopProperty =
+ AvaloniaProperty.Register(nameof(ColorStop));
+
+ ///
+ /// Gets or sets the position reference to use when positioning and dragging this color stop.
+ /// If then dragging is not enabled.
+ ///
+ public static readonly StyledProperty PositionReferenceProperty =
+ AvaloniaProperty.Register(nameof(PositionReference));
+
+ ///
+ /// Gets the linear gradient brush representing the color gradient.
+ ///
+ public static readonly DirectProperty IsSelectedProperty =
+ AvaloniaProperty.RegisterDirect(nameof(IsSelected), g => g.IsSelected);
+
+ private bool _isSelected;
+ private double _dragOffset;
+
+ ///
+ /// Gets or sets the gradient picker.
+ ///
+ public GradientPicker? GradientPicker
+ {
+ get => GetValue(GradientPickerProperty);
+ set => SetValue(GradientPickerProperty, value);
+ }
+
+ ///
+ /// Gets or sets the color stop.
+ ///
+ public ColorGradientStop ColorStop
+ {
+ get => GetValue(ColorStopProperty);
+ set => SetValue(ColorStopProperty, value);
+ }
+
+ ///
+ /// Gets or sets the position reference to use when positioning and dragging this color stop.
+ /// If then dragging is not enabled.
+ ///
+ public IControl? PositionReference
+ {
+ get => GetValue(PositionReferenceProperty);
+ set => SetValue(PositionReferenceProperty, value);
+ }
+
+ ///
+ /// Gets the linear gradient brush representing the color gradient.
+ ///
+ public bool IsSelected
+ {
+ get => _isSelected;
+ private set
+ {
+ SetAndRaise(IsSelectedProperty, ref _isSelected, value);
+ if (IsSelected)
+ PseudoClasses.Add(":selected");
+ else
+ PseudoClasses.Remove(":selected");
+ }
+ }
+
+ private void GradientPickerOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (GradientPicker != null && e.Property == GradientPicker.SelectedColorStopProperty)
+ {
+ IsSelected = GradientPicker.SelectedColorStop == ColorStop;
+ }
+ }
+
+ private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || PositionReference == null)
+ return;
+
+ _dragOffset = e.GetCurrentPoint(PositionReference).Position.X - GetPixelPosition();
+ e.Pointer.Capture(this);
+
+ // Store these in case the control is being recreated due to an array resort
+ // it's a bit ugly but it gives us a way to pick up dragging again with the new control
+ _dragPointer = e.Pointer;
+ _draggingStop = ColorStop;
+ e.Handled = true;
+
+ if (GradientPicker != null)
+ GradientPicker.SelectedColorStop = ColorStop;
+ }
+
+ private void OnPointerMoved(object? sender, PointerEventArgs e)
+ {
+ if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || !ReferenceEquals(e.Pointer.Captured, this) || PositionReference == null)
+ {
+ if (_draggingStop != ColorStop)
+ return;
+
+ _dragOffset = e.GetCurrentPoint(PositionReference).Position.X - GetPixelPosition();
+ }
+
+ double position = e.GetCurrentPoint(PositionReference).Position.X - _dragOffset;
+ ColorStop.Position = MathF.Round((float) Math.Clamp(position / PositionReference.Bounds.Width, 0, 1), 3, MidpointRounding.AwayFromZero);
+ e.Handled = true;
+ }
+
+ private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (e.InitialPressMouseButton != MouseButton.Left)
+ return;
+
+ e.Pointer.Capture(null);
+ e.Handled = true;
+ _draggingStop = null;
+ }
+
+ private double GetPixelPosition()
+ {
+ if (PositionReference == null)
+ return 0;
+
+ return PositionReference.Bounds.Width * ColorStop.Position;
+ }
+
+ #region Overrides of Visual
+
+ ///
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ if (GradientPicker != null)
+ GradientPicker.PropertyChanged += GradientPickerOnPropertyChanged;
+
+ if (PositionReference != null)
+ {
+ PointerPressed += OnPointerPressed;
+ PointerMoved += OnPointerMoved;
+ PointerReleased += OnPointerReleased;
+
+ // If this stop was previously being dragged, carry on dragging again
+ // This can happen if the control was recreated due to an array sort
+ if (_draggingStop == ColorStop && _dragPointer != null)
+ {
+ _dragPointer.Capture(this);
+ IsSelected = true;
+ }
+ }
+
+ base.OnAttachedToVisualTree(e);
+ }
+
+ ///
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ if (GradientPicker != null)
+ GradientPicker.PropertyChanged -= GradientPickerOnPropertyChanged;
+ PointerPressed -= OnPointerPressed;
+ PointerMoved -= OnPointerMoved;
+ PointerReleased -= OnPointerReleased;
+
+ base.OnDetachedFromVisualTree(e);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs
new file mode 100644
index 000000000..29f562d47
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Artemis.Core;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using SkiaSharp;
+
+namespace Artemis.UI.Shared.Converters;
+
+///
+/// Converts into a .
+///
+public class ColorGradientToGradientStopsConverter : IValueConverter
+{
+ ///
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ ColorGradient? colorGradient = value as ColorGradient;
+ GradientStops collection = new();
+ if (colorGradient == null)
+ return collection;
+
+ foreach (ColorGradientStop c in colorGradient.OrderBy(s => s.Position))
+ collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
+ return collection;
+ }
+
+ ///
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ GradientStops? collection = value as GradientStops;
+ ColorGradient colorGradients = new();
+ if (collection == null)
+ return colorGradients;
+
+ foreach (GradientStop c in collection.OrderBy(s => s.Offset))
+ colorGradients.Add(new ColorGradientStop(new SKColor(c.Color.R, c.Color.G, c.Color.B, c.Color.A), (float) c.Offset));
+ return colorGradients;
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs
new file mode 100644
index 000000000..2d8219d0e
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Converters/ParentWidthPercentageConverter.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Globalization;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+
+namespace Artemis.UI.Shared.Converters;
+
+///
+/// Converts the width in percentage to a real number based on the width of the given parent
+///
+public class ParentWidthPercentageConverter : IValueConverter
+{
+ #region Implementation of IValueConverter
+
+ ///
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (parameter is not IControl parent || value is not double percentage)
+ return value;
+
+ return parent.Width / 100.0 * percentage;
+ }
+
+ ///
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (parameter is not IControl parent || value is not double real)
+ return value;
+
+ return real / parent.Width * 100.0;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs
similarity index 95%
rename from src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs
rename to src/Avalonia/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs
index e7707d8f1..7837c188e 100644
--- a/src/Avalonia/Artemis.UI/Converters/SKColorToStringConverter.cs
+++ b/src/Avalonia/Artemis.UI.Shared/Converters/SKColorToStringConverter.cs
@@ -3,7 +3,7 @@ using System.Globalization;
using Avalonia.Data.Converters;
using SkiaSharp;
-namespace Artemis.UI.Converters;
+namespace Artemis.UI.Shared.Converters;
///
///
diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs
new file mode 100644
index 000000000..3e3021ecf
--- /dev/null
+++ b/src/Avalonia/Artemis.UI.Shared/Converters/WidthNormalizedConverter.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Data.Converters;
+
+namespace Artemis.UI.Shared.Converters;
+
+///
+/// Converts the width in percentage to a real number based on the width of the given parent
+///
+public class WidthNormalizedConverter : IMultiValueConverter
+{
+ #region Implementation of IMultiValueConverter
+
+ ///
+ public object? Convert(IList