diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs new file mode 100644 index 0000000000..ad8f66251a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs @@ -0,0 +1,112 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Creates an accent color for a given base color value and step parameter. + /// + public class AccentColorConverter : IValueConverter + { + /// + /// The amount to change the Value channel for each accent color step. + /// + public const double ValueDelta = 0.1; + + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + int accentStep; + Color? rgbColor = null; + HsvColor? hsvColor = null; + + // Get the current color in HSV + if (value is Color valueColor) + { + rgbColor = valueColor; + } + else if (value is HsvColor valueHsvColor) + { + hsvColor = valueHsvColor; + } + else if (value is SolidColorBrush valueBrush) + { + rgbColor = valueBrush.Color; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + // Get the value component delta + try + { + accentStep = int.Parse(parameter?.ToString() ?? "", CultureInfo.InvariantCulture); + } + catch + { + // Invalid parameter provided, unable to convert to integer + return AvaloniaProperty.UnsetValue; + } + + if (hsvColor == null && + rgbColor != null) + { + hsvColor = rgbColor.Value.ToHsv(); + } + + if (hsvColor != null) + { + return new SolidColorBrush(GetAccent(hsvColor.Value, accentStep).ToRgb()); + } + else + { + return AvaloniaProperty.UnsetValue; + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + + /// + /// This does not account for perceptual differences and also does not match with + /// system accent color calculation. + /// + /// + /// Use the HSV representation as it's more perceptual. + /// In most cases only the value is changed by a fixed percentage so the algorithm is reproducible. + /// + /// The base color to calculate the accent from. + /// The number of accent color steps to move. + /// The new accent color. + public static HsvColor GetAccent(HsvColor hsvColor, int accentStep) + { + if (accentStep != 0) + { + double colorValue = hsvColor.V; + colorValue += (accentStep * AccentColorConverter.ValueDelta); + colorValue = Math.Round(colorValue, 2); + + return new HsvColor(hsvColor.A, hsvColor.H, hsvColor.S, colorValue); + } + else + { + return hsvColor; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs new file mode 100644 index 0000000000..74c0943919 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -0,0 +1,69 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorPreviewer + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White); + + /// + /// Gets or sets the currently previewed color in the RGB color model. + /// + /// + /// For control authors use instead to avoid loss + /// of precision and color drifting. + /// + public Color Color + { + get => GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.Transparent.ToHsv()); + + /// + /// Gets or sets the currently previewed color in the HSV color model. + /// + /// + /// This should be used in all cases instead of the property. + /// Internally, the uses the HSV color model and using + /// this property will avoid loss of precision and color drifting. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + + /// + /// Gets or sets a value indicating whether accent colors are shown along + /// with the preview color. + /// + public bool ShowAccentColors + { + get => (bool)this.GetValue(ShowAccentColorsProperty); + set => SetValue(ShowAccentColorsProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs new file mode 100644 index 0000000000..35bd62601f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -0,0 +1,138 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Metadata; +using Avalonia.Input; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Presents a preview color with optional accent colors. + /// + [TemplatePart(Name = nameof(AccentDec1Border), Type = typeof(Border))] + [TemplatePart(Name = nameof(AccentDec2Border), Type = typeof(Border))] + [TemplatePart(Name = nameof(AccentInc1Border), Type = typeof(Border))] + [TemplatePart(Name = nameof(AccentInc2Border), Type = typeof(Border))] + public partial class ColorPreviewer : TemplatedControl + { + /// + /// Event for when the selected color changes within the previewer. + /// This happens when an accent color is pressed. + /// + public event EventHandler? ColorChanged; + + private bool eventsConnected = false; + + private Border? AccentDec1Border; + private Border? AccentDec2Border; + private Border? AccentInc1Border; + private Border? AccentInc2Border; + + /// + /// Initializes a new instance of the class. + /// + public ColorPreviewer() : base() + { + } + + /// + /// Connects or disconnects all control event handlers. + /// + /// True to connect event handlers, otherwise false. + private void ConnectEvents(bool connected) + { + if (connected == true && eventsConnected == false) + { + // Add all events + if (AccentDec1Border != null) { AccentDec1Border.PointerPressed += AccentBorder_PointerPressed; } + if (AccentDec2Border != null) { AccentDec2Border.PointerPressed += AccentBorder_PointerPressed; } + if (AccentInc1Border != null) { AccentInc1Border.PointerPressed += AccentBorder_PointerPressed; } + if (AccentInc2Border != null) { AccentInc2Border.PointerPressed += AccentBorder_PointerPressed; } + + eventsConnected = true; + } + else if (connected == false && eventsConnected == true) + { + // Remove all events + if (AccentDec1Border != null) { AccentDec1Border.PointerPressed -= AccentBorder_PointerPressed; } + if (AccentDec2Border != null) { AccentDec2Border.PointerPressed -= AccentBorder_PointerPressed; } + if (AccentInc1Border != null) { AccentInc1Border.PointerPressed -= AccentBorder_PointerPressed; } + if (AccentInc2Border != null) { AccentInc2Border.PointerPressed -= AccentBorder_PointerPressed; } + + eventsConnected = false; + } + + return; + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + // Remove any existing events present if the control was previously loaded then unloaded + ConnectEvents(false); + + AccentDec1Border = e.NameScope.Find(nameof(AccentDec1Border)); + AccentDec2Border = e.NameScope.Find(nameof(AccentDec2Border)); + AccentInc1Border = e.NameScope.Find(nameof(AccentInc1Border)); + AccentInc2Border = e.NameScope.Find(nameof(AccentInc2Border)); + + // Must connect after controls are found + ConnectEvents(true); + + base.OnApplyTemplate(e); + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + // Always keep the two color properties in sync + if (change.Property == ColorProperty) + { + HsvColor = Color.ToHsv(); + } + else if (change.Property == HsvColorProperty) + { + Color = HsvColor.ToRgb(); + } + + base.OnPropertyChanged(change); + } + + /// + /// Called before the event occurs. + /// + /// The newly selected color. + protected virtual void OnColorChanged(HsvColor newColor) + { + var oldColor = HsvColor; + HsvColor = newColor; + + ColorChanged?.Invoke(this, new ColorChangedEventArgs(oldColor.ToRgb(), newColor.ToRgb())); + + return; + } + + /// + /// Event handler for when an accent color border is pressed. + /// This will update the color to the background of the pressed panel. + /// + private void AccentBorder_PointerPressed(object? sender, PointerPressedEventArgs e) + { + Border? border = sender as Border; + int accentStep = 0; + HsvColor hsvColor = HsvColor; + + // Get the value component delta + try + { + accentStep = int.Parse(border?.Tag?.ToString() ?? "", CultureInfo.InvariantCulture); + } + catch { } + + HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); + OnColorChanged(newHsvColor); + + return; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index fb656ce964..d96066e56b 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -21,6 +21,7 @@ + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml new file mode 100644 index 0000000000..a91da45578 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -0,0 +1,90 @@ + + + + + + + + +