From f33fe3b708f1d6a5966b4ba71447d35165a4aeaf Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 21 Apr 2022 20:25:16 +0200 Subject: [PATCH 01/41] Avoid checking all array values. --- src/Avalonia.Base/Utilities/WeakHashList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/WeakHashList.cs b/src/Avalonia.Base/Utilities/WeakHashList.cs index df480aa062..fe582e8a78 100644 --- a/src/Avalonia.Base/Utilities/WeakHashList.cs +++ b/src/Avalonia.Base/Utilities/WeakHashList.cs @@ -118,7 +118,7 @@ internal class WeakHashList where T : class { if (_arr != null) { - for (var c = 0; c < _arr.Length; c++) + for (var c = 0; c < _arrCount; c++) { if (_arr[c]?.TryGetTarget(out var target) == true && target == item) { From f63ec5f5a915789a0dff264585ec895bd1b3cf61 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:47:51 -0400 Subject: [PATCH 02/41] Separate color picker control styles --- .../Themes/Default.xaml | 133 +--------------- .../Themes/Default/ColorSpectrum.xaml | 134 ++++++++++++++++ .../Themes/Fluent.xaml | 146 +++--------------- .../Themes/Fluent/ColorSpectrum.xaml | 134 ++++++++++++++++ 4 files changed, 290 insertions(+), 257 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 832daf8853..528eed9969 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,134 +1,7 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml new file mode 100644 index 0000000000..9596ca9653 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index 545702ea84..fb656ce964 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -1,134 +1,26 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml new file mode 100644 index 0000000000..b209fe75b3 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 21dd8392bf95045d404dbb67a4e5c589d691eb27 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:48:39 -0400 Subject: [PATCH 03/41] Add initial ColorPreviewer primitive --- .../ColorPreviewer/AccentColorConverter.cs | 112 ++++++++++++++ .../ColorPreviewer.Properties.cs | 69 +++++++++ .../ColorPreviewer/ColorPreviewer.cs | 138 ++++++++++++++++++ .../Themes/Fluent.xaml | 1 + .../Themes/Fluent/ColorPreviewer.xaml | 90 ++++++++++++ 5 files changed, 410 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml 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 @@ + + + + + + + + + From ad5249992df8667f2785cf2504b2ddcc62a730c8 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:49:24 -0400 Subject: [PATCH 04/41] Add initial ColorSlider primitive --- .../ColorComponent.cs | 28 ++ .../{ColorSpectrum => }/ColorHelpers.cs | 344 +++++++++++++++++- .../ColorModel.cs | 18 + .../ColorSlider/ColorSlider.Properties.cs | 143 ++++++++ .../ColorSlider/ColorSlider.cs | 221 +++++++++++ .../Themes/Fluent.xaml | 1 + .../Themes/Fluent/ColorSlider.xaml | 172 +++++++++ 7 files changed, 925 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorComponent.cs rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => }/ColorHelpers.cs (50%) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorModel.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs create mode 100644 src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml diff --git a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs new file mode 100644 index 0000000000..a0385c03b4 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs @@ -0,0 +1,28 @@ +namespace Avalonia.Controls +{ + /// + /// Defines a specific component within a color model. + /// + public enum ColorComponent + { + /// + /// Represents the alpha component. + /// + Alpha, + + /// + /// Represents the first color component which is Red when RGB or Hue when HSV. + /// + Component1, + + /// + /// Represents the second color component which is Green when RGB or Saturation when HSV. + /// + Component2, + + /// + /// Represents the third color component which is Blue when RGB or Value when HSV. + /// + Component3 + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/ColorHelpers.cs similarity index 50% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs rename to src/Avalonia.Controls.ColorPicker/ColorHelpers.cs index b912d39aba..37c6f552d6 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorHelpers.cs @@ -6,9 +6,12 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { @@ -26,6 +29,291 @@ namespace Avalonia.Controls.Primitives return string.Empty; } + /// + /// Generates a new bitmap of the specified size by changing a specific color component. + /// This will produce a gradient representing a sweep of all possible values of the color component. + /// + /// The pixel width (X, horizontal) of the resulting bitmap. + /// The pixel height (Y, vertical) of the resulting bitmap. + /// The orientation of the resulting bitmap (gradient direction). + /// The color model being used: RGBA or HSVA. + /// The specific color component to sweep. + /// The base HSV color used for components not being changed. + /// Fix the alpha component value to maximum during calculation. + /// This will remove any alpha/transparency from the other component backgrounds. + /// Fix the saturation and value components to maximum + /// during calculation with the HSVA color model. + /// This will ensure colors are always discernible regardless of saturation/value. + /// A new bitmap representing a gradient of color component values. + internal static async Task CreateComponentBitmapAsync( + int width, + int height, + Orientation orientation, + ColorModel colorModel, + ColorComponent component, + HsvColor baseHsvColor, + bool isAlphaMaxForced, + bool isSaturationValueMaxForced) + { + if (width == 0 || height == 0) + { + return new byte[0]; + } + + var bitmap = await Task.Run(() => + { + int pixelDataIndex = 0; + double componentStep; + byte[] bgraPixelData; + Color baseRgbColor = Colors.White; + Color rgbColor; + int bgraPixelDataHeight; + int bgraPixelDataWidth; + + // Allocate the buffer + // BGRA formatted color components 1 byte each (4 bytes in a pixel) + bgraPixelData = new byte[width * height * 4]; + bgraPixelDataHeight = height * 4; + bgraPixelDataWidth = width * 4; + + // Maximize alpha component value + if (isAlphaMaxForced && + component != ColorComponent.Alpha) + { + baseHsvColor = new HsvColor(1.0, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + } + + // Convert HSV to RGB once + if (colorModel == ColorModel.Rgba) + { + baseRgbColor = baseHsvColor.ToRgb(); + } + + // Maximize Saturation and Value components when in HSVA mode + if (isSaturationValueMaxForced && + colorModel == ColorModel.Hsva && + component != ColorComponent.Alpha) + { + switch (component) + { + case ColorComponent.Component1: + baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0); + break; + case ColorComponent.Component2: + baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0); + break; + case ColorComponent.Component3: + baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V); + break; + } + } + + // Create the color component gradient + if (orientation == Orientation.Horizontal) + { + // Determine the numerical increment of the color steps within the component + if (colorModel == ColorModel.Hsva) + { + if (component == ColorComponent.Component1) + { + componentStep = 360.0 / width; + } + else + { + componentStep = 1.0 / width; + } + } + else + { + componentStep = 255.0 / width; + } + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (y == 0) + { + rgbColor = GetColor(x * componentStep); + + // Get a new color + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 3] = rgbColor.A; + } + else + { + // Use the color in the row above + // Remember the pixel data is 1 dimensional instead of 2 + bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex + 0 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex + 1 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex + 2 - bgraPixelDataWidth]; + bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex + 3 - bgraPixelDataWidth]; + } + + pixelDataIndex += 4; + } + } + } + else + { + // Determine the numerical increment of the color steps within the component + if (colorModel == ColorModel.Hsva) + { + if (component == ColorComponent.Component1) + { + componentStep = 360.0 / height; + } + else + { + componentStep = 1.0 / height; + } + } + else + { + componentStep = 255.0 / height; + } + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + if (x == 0) + { + // The lowest component value should be at the 'bottom' of the bitmap + rgbColor = GetColor((height - 1 - y) * componentStep); + + // Get a new color + bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); + bgraPixelData[pixelDataIndex + 3] = rgbColor.A; + } + else + { + // Use the color in the column to the left + // Remember the pixel data is 1 dimensional instead of 2 + bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex - 4]; + bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex - 3]; + bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex - 2]; + bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex - 1]; + } + + pixelDataIndex += 4; + } + } + } + + Color GetColor(double componentValue) + { + Color newRgbColor = Colors.White; + + switch (component) + { + case ColorComponent.Component1: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep hue + newRgbColor = HsvColor.ToRgb( + MathUtilities.Clamp(componentValue, 0.0, 360.0), + baseHsvColor.S, + baseHsvColor.V, + baseHsvColor.A); + } + else + { + // Sweep red + newRgbColor = new Color( + baseRgbColor.A, + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), + baseRgbColor.G, + baseRgbColor.B); + } + + break; + } + case ColorComponent.Component2: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep saturation + newRgbColor = HsvColor.ToRgb( + baseHsvColor.H, + MathUtilities.Clamp(componentValue, 0.0, 1.0), + baseHsvColor.V, + baseHsvColor.A); + } + else + { + // Sweep green + newRgbColor = new Color( + baseRgbColor.A, + baseRgbColor.R, + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), + baseRgbColor.B); + } + + break; + } + case ColorComponent.Component3: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep value + newRgbColor = HsvColor.ToRgb( + baseHsvColor.H, + baseHsvColor.S, + MathUtilities.Clamp(componentValue, 0.0, 1.0), + baseHsvColor.A); + } + else + { + // Sweep blue + newRgbColor = new Color( + baseRgbColor.A, + baseRgbColor.R, + baseRgbColor.G, + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0))); + } + + break; + } + case ColorComponent.Alpha: + { + if (colorModel == ColorModel.Hsva) + { + // Sweep alpha + newRgbColor = HsvColor.ToRgb( + baseHsvColor.H, + baseHsvColor.S, + baseHsvColor.V, + MathUtilities.Clamp(componentValue, 0.0, 1.0)); + } + else + { + // Sweep alpha + newRgbColor = new Color( + Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), + baseRgbColor.R, + baseRgbColor.G, + baseRgbColor.B); + } + + break; + } + } + + return newRgbColor; + } + + return bgraPixelData; + }); + + return bitmap; + } + public static Hsv IncrementColorComponent( Hsv originalHsv, HsvComponent component, @@ -363,14 +651,22 @@ namespace Avalonia.Controls.Primitives return originalAlpha / 100; } + /// + /// + /// + /// The pixel width of the bitmap. + /// The pixel height of the bitmap. + /// + /// public static WriteableBitmap CreateBitmapFromPixelData( int pixelWidth, int pixelHeight, List bgraPixelData) { - Vector dpi = new Vector(96, 96); // Standard may need to change on some devices + // Standard may need to change on some devices + Vector dpi = new Vector(96, 96); - WriteableBitmap bitmap = new WriteableBitmap( + var bitmap = new WriteableBitmap( new PixelSize(pixelWidth, pixelHeight), dpi, PixelFormat.Bgra8888, @@ -385,6 +681,50 @@ namespace Avalonia.Controls.Primitives return bitmap; } + /// + /// Converts the given bitmap (in raw BGRA pre-multiplied alpha pixels) into an image brush + /// that can be used in the UI. + /// + /// The bitmap (in raw BGRA pre-multiplied alpha pixels) + /// to convert to a brush. + /// The pixel width of the bitmap. + /// The pixel height of the bitmap. + /// A new . + public static IBrush? BitmapToBrushAsync( + byte[] bgraPixelData, + int pixelWidth, + int pixelHeight) + { + if (bgraPixelData.Length == 0 || + (pixelWidth == 0 && + pixelHeight == 0)) + { + return null; + } + + // Standard may need to change on some devices + Vector dpi = new Vector(96, 96); + + var bitmap = new WriteableBitmap( + new PixelSize(pixelWidth, pixelHeight), + dpi, + PixelFormat.Bgra8888, + AlphaFormat.Premul); + + // Warning: This is highly questionable + using (var frameBuffer = bitmap.Lock()) + { + Marshal.Copy(bgraPixelData, 0, frameBuffer.Address, bgraPixelData.Length); + } + + var brush = new ImageBrush(bitmap) + { + Stretch = Stretch.Fill + }; + + return brush; + } + /// /// Gets the relative (perceptual) luminance/brightness of the given color. /// 1 is closer to white while 0 is closer to black. diff --git a/src/Avalonia.Controls.ColorPicker/ColorModel.cs b/src/Avalonia.Controls.ColorPicker/ColorModel.cs new file mode 100644 index 0000000000..f11b514706 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorModel.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Controls +{ + /// + /// Defines the model used to represent colors. + /// + public enum ColorModel + { + /// + /// Color is represented by hue, saturation, value and alpha components. + /// + Hsva, + + /// + /// Color is represented by red, green, blue and alpha components. + /// + Rgba + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs new file mode 100644 index 0000000000..3aa3e3a789 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -0,0 +1,143 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorSlider + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White); + + /// + /// Gets or sets the currently selected color in the RGB color model. + /// + /// + /// Use this property instead of when in + /// 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 ColorComponentProperty = + AvaloniaProperty.Register( + nameof(ColorComponent), + ColorComponent.Component1); + + /// + /// Gets or sets the color component represented by the slider. + /// + public ColorComponent ColorComponent + { + get => GetValue(ColorComponentProperty); + set => SetValue(ColorComponentProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + + /// + /// Gets or sets the active color model used by the slider. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv()); + + /// + /// Gets or sets the currently selected color in the HSV color model. + /// + /// + /// Use this property instead of when in + /// to avoid loss of precision and color drifting. + /// + public HsvColor HsvColor + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsAlphaMaxForced), + true); + + /// + /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components + /// other than . + /// This ensures that the background is always visible and never transparent regardless of the actual color. + /// + public bool IsAlphaMaxForced + { + get => GetValue(IsAlphaMaxForcedProperty); + set => SetValue(IsAlphaMaxForcedProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAutoUpdatingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAutoUpdatingEnabled), + true); + + /// + /// Gets or sets a value indicating whether automatic background and foreground updates will be + /// calculated when the set color changes. + /// + /// + /// This can be disabled for performance reasons when working with multiple sliders. + /// + public bool IsAutoUpdatingEnabled + { + get => GetValue(IsAutoUpdatingEnabledProperty); + set => SetValue(IsAutoUpdatingEnabledProperty, value); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsSaturationValueMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsSaturationValueMaxForced), + true); + + /// + /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values + /// when using the HSVA color model. Only component values other than will be changed. + /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color. + /// + public bool IsSaturationValueMaxForced + { + get => GetValue(IsSaturationValueMaxForcedProperty); + set => SetValue(IsSaturationValueMaxForcedProperty, value); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs new file mode 100644 index 0000000000..e3f2dc3555 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -0,0 +1,221 @@ +using System; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Primitives +{ + /// + /// A slider with a background that represents a single color component. + /// + public partial class ColorSlider : Slider + { + private Size cachedSize = Size.Empty; + + /// + /// Initializes a new instance of the class. + /// + public ColorSlider() : base() + { + } + + /// + /// Update the slider's Foreground and Background brushes based on the current slider state and color. + /// + /// + /// Manually refreshes the background gradient of the slider. + /// This is callable separately for performance reasons. + /// + public void UpdateColors() + { + HsvColor hsvColor = HsvColor; + + // Calculate and set the background + UpdateBackground(hsvColor); + + // Calculate and set the foreground ensuring contrast with the background + Color rgbColor = hsvColor.ToRgb(); + Color selectedRgbColor; + double sliderPercent = Value / (Maximum - Minimum); + + var component = ColorComponent; + + if (ColorModel == ColorModel.Hsva) + { + if (IsAlphaMaxForced && + component != ColorComponent.Alpha) + { + hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); + } + + switch (component) + { + case ColorComponent.Component1: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); + + hsvColor = new HsvColor( + hsvColor.A, + componentValue, + IsSaturationValueMaxForced ? 1.0 : hsvColor.S, + IsSaturationValueMaxForced ? 1.0 : hsvColor.V); + + break; + } + + case ColorComponent.Component2: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + + hsvColor = new HsvColor( + hsvColor.A, + hsvColor.H, + componentValue, + IsSaturationValueMaxForced ? 1.0 : hsvColor.V); + + break; + } + + case ColorComponent.Component3: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + + hsvColor = new HsvColor( + hsvColor.A, + hsvColor.H, + IsSaturationValueMaxForced ? 1.0 : hsvColor.S, + componentValue); + + break; + } + } + + selectedRgbColor = hsvColor.ToRgb(); + } + else + { + if (IsAlphaMaxForced && + component != ColorComponent.Alpha) + { + rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); + } + + byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255)); + + switch (component) + { + case ColorComponent.Component1: + rgbColor = new Color(rgbColor.A, componentValue, rgbColor.G, rgbColor.B); + break; + case ColorComponent.Component2: + rgbColor = new Color(rgbColor.A, rgbColor.R, componentValue, rgbColor.B); + break; + case ColorComponent.Component3: + rgbColor = new Color(rgbColor.A, rgbColor.R, rgbColor.G, componentValue); + break; + } + + selectedRgbColor = rgbColor; + } + + //var converter = new ContrastBrushConverter(); + //this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush; + + return; + } + + /// + /// Generates a new background image for the color slider and applies it. + /// + private async void UpdateBackground(HsvColor color) + { + // Updates may be requested when sliders are not in the visual tree. + // For first-time load this is handled by the Loaded event. + // However, after that problems may arise, consider the following case: + // + // (1) Backgrounds are drawn normally the first time on Loaded. + // Actual height/width are available. + // (2) The palette tab is selected which has no sliders + // (3) The picker flyout is closed + // (4) Externally the color is changed + // The color change will trigger slider background updates but + // with the flyout closed, actual height/width are zero. + // No zero size bitmap can be generated. + // (5) The picker flyout is re-opened by the user and the default + // last-opened tab will be viewed: palette. + // No loaded events will be fired for sliders. The color change + // event was already handled in (4). The sliders will never + // be updated. + // + // In this case the sliders become out of sync with the Color because there is no way + // to tell when they actually come into view. To work around this, force a re-render of + // the background with the last size of the slider. This last size will be when it was + // last loaded or updated. + // + // In the future additional consideration may be required for SizeChanged of the control. + // This work-around will also cause issues if display scaling changes in the special + // case where cached sizes are required. + + var width = Convert.ToInt32(Bounds.Width); + var height = Convert.ToInt32(Bounds.Height); + + if (width == 0 || height == 0) + { + // Attempt to use the last size if it was available + if (cachedSize.IsDefault == false) + { + width = Convert.ToInt32(cachedSize.Width); + height = Convert.ToInt32(cachedSize.Height); + } + } + else + { + cachedSize = new Size(width, height); + } + + var bitmap = await ColorHelpers.CreateComponentBitmapAsync( + width, + height, + Orientation, + ColorModel, + ColorComponent, + color, + IsAlphaMaxForced, + IsSaturationValueMaxForced); + + if (bitmap != null) + { + Background = ColorHelpers.BitmapToBrushAsync(bitmap, width, height); + } + + return; + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + bool update = false; + + if (change.Property == ColorProperty) + { + // Sync with HSV (which is primary) + HsvColor = Color.ToHsv(); + update = true; + } + else if (change.Property == HsvColorProperty) + { + update = true; + } + else if (change.Property == BoundsProperty) + { + update = true; + } + + if (update && IsAutoUpdatingEnabled) + { + UpdateColors(); + } + + base.OnPropertyChanged(change); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml index d96066e56b..c25d79727f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml @@ -22,6 +22,7 @@ + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml new file mode 100644 index 0000000000..1ca9b12ffe --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + From a5b3e85dfc519bee6a762e2d5c45869002e0d135 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:50:00 -0400 Subject: [PATCH 05/41] Simplify default color property values --- .../ColorSpectrum/ColorSpectrum.Properties.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 824bf9ab05..ab5b83afcb 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty ColorProperty = AvaloniaProperty.Register( nameof(Color), - Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); + Colors.White); /// /// Gets or sets the two HSV color components displayed by the spectrum. @@ -71,7 +71,7 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty HsvColorProperty = AvaloniaProperty.Register( nameof(HsvColor), - new HsvColor(1, 0, 0, 1)); + Colors.White.ToHsv()); /// /// Gets or sets the maximum value of the Hue component in the range from 0..359. From e9cb628f820ba7226cce379a601b9867053eef76 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:50:22 -0400 Subject: [PATCH 06/41] Improve formatting --- .../Converters/CornerRadiusFilterConverter.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs index b2433bfd97..a91f143019 100644 --- a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs +++ b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; - using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters @@ -22,7 +21,12 @@ namespace Avalonia.Controls.Converters /// public double Scale { get; set; } = 1; - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { if (!(value is CornerRadius radius)) { @@ -36,7 +40,12 @@ namespace Avalonia.Controls.Converters Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0); } - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) { throw new NotImplementedException(); } From c068adb60944b8e6a2ba1bfc1a3666c8e0caf1fd Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:51:01 -0400 Subject: [PATCH 07/41] Update ColorPickerPage in ControlCatalog with ColorSlider and ColorPreviewer --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index ec34193f8c..f343fd8f59 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -3,14 +3,18 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls" - mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + mc:Ignorable="d" + d:DesignWidth="800" + d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - + + + + + + + From b6b8e96fd97a5d77d7e26ab2e189ab9d84849e48 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 18:56:49 -0400 Subject: [PATCH 08/41] Move spectrum enums into ColorSpectrum directory --- .../{ => ColorSpectrum}/ColorSpectrumComponents.cs | 0 .../{ => ColorSpectrum}/ColorSpectrumShape.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ => ColorSpectrum}/ColorSpectrumComponents.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ => ColorSpectrum}/ColorSpectrumShape.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs rename to src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs From 1f5e3c0d9dcfa2f95ad2d217521f075d33364d5c Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 19:28:21 -0400 Subject: [PATCH 09/41] Move helpers into separate directory --- src/Avalonia.Controls.ColorPicker/{ => Helpers}/ColorHelpers.cs | 0 .../{ColorSpectrum => Helpers}/Hsv.cs | 0 .../{ColorSpectrum => Helpers}/IncrementAmount.cs | 0 .../{ColorSpectrum => Helpers}/IncrementDirection.cs | 0 .../{ColorSpectrum => Helpers}/Rgb.cs | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ => Helpers}/ColorHelpers.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/Hsv.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/IncrementAmount.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/IncrementDirection.cs (100%) rename src/Avalonia.Controls.ColorPicker/{ColorSpectrum => Helpers}/Rgb.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorHelpers.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs b/src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs b/src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs b/src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs b/src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs rename to src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs From 65e5e580acd359c0103e7b9bb3b0397d7676f1e1 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 21:57:19 -0400 Subject: [PATCH 10/41] Make ColorSlider fully functional --- .../ControlCatalog/Pages/ColorPickerPage.xaml | 3 +- .../ColorSlider/ColorSlider.cs | 367 ++++++++++++------ .../Helpers/ColorHelpers.cs | 2 +- 3 files changed, 241 insertions(+), 131 deletions(-) diff --git a/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index f343fd8f59..09ec15ad15 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -25,7 +25,8 @@ Width="256" /> + RowDefinitions="Auto,Auto,Auto,Auto,Auto" + Margin="0,10,0,0"> public partial class ColorSlider : Slider { - private Size cachedSize = Size.Empty; + private bool disableUpdates = false; /// /// Initializes a new instance of the class. @@ -19,200 +20,308 @@ namespace Avalonia.Controls.Primitives } /// - /// Update the slider's Foreground and Background brushes based on the current slider state and color. + /// Generates a new background image for the color slider and applies it. /// - /// - /// Manually refreshes the background gradient of the slider. - /// This is callable separately for performance reasons. - /// - public void UpdateColors() + private async void UpdateBackground() { - HsvColor hsvColor = HsvColor; + // In Avalonia, Bounds returns the actual device-independent pixel size of a control. + // However, this is not necessarily the size of the control rendered on a display. + // A desktop or application scaling factor may be applied which must be accounted for here. + // Remember bitmaps in Avalonia are rendered mapping to actual device pixels, not the device- + // independent pixels of controls. - // Calculate and set the background - UpdateBackground(hsvColor); + var scale = LayoutHelper.GetLayoutScale(this); + var pixelWidth = Convert.ToInt32(Bounds.Width * scale); + var pixelHeight = Convert.ToInt32(Bounds.Height * scale); - // Calculate and set the foreground ensuring contrast with the background - Color rgbColor = hsvColor.ToRgb(); - Color selectedRgbColor; - double sliderPercent = Value / (Maximum - Minimum); + if (pixelWidth != 0 && pixelHeight != 0) + { + var bitmap = await ColorHelpers.CreateComponentBitmapAsync( + pixelWidth, + pixelHeight, + Orientation, + ColorModel, + ColorComponent, + HsvColor, + IsAlphaMaxForced, + IsSaturationValueMaxForced); + + if (bitmap != null) + { + Background = ColorHelpers.BitmapToBrushAsync(bitmap, pixelWidth, pixelHeight); + } + } + + return; + } + /// + /// Updates the slider property values by applying the current color. + /// + /// + /// Warning: This will trigger property changed updates. + /// Consider using externally. + /// + private void SetColorToSliderValues() + { + var hsvColor = HsvColor; + var rgbColor = Color; var component = ColorComponent; if (ColorModel == ColorModel.Hsva) { - if (IsAlphaMaxForced && - component != ColorComponent.Alpha) + // Note: Components converted into a usable range for the user + switch (component) { - hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); + case ColorComponent.Alpha: + Minimum = 0; + Maximum = 100; + Value = hsvColor.A * 100; + break; + case ColorComponent.Component1: // Hue + Minimum = 0; + Maximum = 359; + Value = hsvColor.H; + break; + case ColorComponent.Component2: // Saturation + Minimum = 0; + Maximum = 100; + Value = hsvColor.S * 100; + break; + case ColorComponent.Component3: // Value + Minimum = 0; + Maximum = 100; + Value = hsvColor.V * 100; + break; } - + } + else + { switch (component) { - case ColorComponent.Component1: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); - - hsvColor = new HsvColor( - hsvColor.A, - componentValue, - IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - IsSaturationValueMaxForced ? 1.0 : hsvColor.V); - - break; - } + case ColorComponent.Alpha: + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.A); + break; + case ColorComponent.Component1: // Red + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.R); + break; + case ColorComponent.Component2: // Green + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.G); + break; + case ColorComponent.Component3: // Blue + Minimum = 0; + Maximum = 255; + Value = Convert.ToDouble(rgbColor.B); + break; + } + } - case ColorComponent.Component2: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + return; + } - hsvColor = new HsvColor( - hsvColor.A, - hsvColor.H, - componentValue, - IsSaturationValueMaxForced ? 1.0 : hsvColor.V); + /// + /// Gets the current color determined by the slider values. + /// + private (Color, HsvColor) GetColorFromSliderValues() + { + HsvColor hsvColor = new HsvColor(); + Color rgbColor = new Color(); + double sliderPercent = Value / (Maximum - Minimum); - break; - } + var baseHsvColor = HsvColor; + var baseRgbColor = Color; + var component = ColorComponent; + if (ColorModel == ColorModel.Hsva) + { + switch (component) + { + case ColorComponent.Alpha: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(componentValue, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component1: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); + hsvColor = new HsvColor(baseHsvColor.A, componentValue, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component2: + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, componentValue, baseHsvColor.V); + break; + } case ColorComponent.Component3: - { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - - hsvColor = new HsvColor( - hsvColor.A, - hsvColor.H, - IsSaturationValueMaxForced ? 1.0 : hsvColor.S, - componentValue); - - break; - } + { + var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, componentValue); + break; + } } - selectedRgbColor = hsvColor.ToRgb(); + return (hsvColor.ToRgb(), hsvColor); } else { - if (IsAlphaMaxForced && - component != ColorComponent.Alpha) - { - rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); - } - byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255)); switch (component) { + case ColorComponent.Alpha: + rgbColor = new Color(componentValue, baseRgbColor.R, baseRgbColor.G, baseRgbColor.B); + break; case ColorComponent.Component1: - rgbColor = new Color(rgbColor.A, componentValue, rgbColor.G, rgbColor.B); + rgbColor = new Color(baseRgbColor.A, componentValue, baseRgbColor.G, baseRgbColor.B); break; case ColorComponent.Component2: - rgbColor = new Color(rgbColor.A, rgbColor.R, componentValue, rgbColor.B); + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, componentValue, baseRgbColor.B); break; case ColorComponent.Component3: - rgbColor = new Color(rgbColor.A, rgbColor.R, rgbColor.G, componentValue); + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, baseRgbColor.G, componentValue); break; } - selectedRgbColor = rgbColor; + return (rgbColor, rgbColor.ToHsv()); } - - //var converter = new ContrastBrushConverter(); - //this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush; - - return; } /// - /// Generates a new background image for the color slider and applies it. + /// Gets the actual background color displayed for the given HSV color. + /// This can differ due to the effects of certain properties intended to improve perception. /// - private async void UpdateBackground(HsvColor color) + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor) { - // Updates may be requested when sliders are not in the visual tree. - // For first-time load this is handled by the Loaded event. - // However, after that problems may arise, consider the following case: - // - // (1) Backgrounds are drawn normally the first time on Loaded. - // Actual height/width are available. - // (2) The palette tab is selected which has no sliders - // (3) The picker flyout is closed - // (4) Externally the color is changed - // The color change will trigger slider background updates but - // with the flyout closed, actual height/width are zero. - // No zero size bitmap can be generated. - // (5) The picker flyout is re-opened by the user and the default - // last-opened tab will be viewed: palette. - // No loaded events will be fired for sliders. The color change - // event was already handled in (4). The sliders will never - // be updated. - // - // In this case the sliders become out of sync with the Color because there is no way - // to tell when they actually come into view. To work around this, force a re-render of - // the background with the last size of the slider. This last size will be when it was - // last loaded or updated. - // - // In the future additional consideration may be required for SizeChanged of the control. - // This work-around will also cause issues if display scaling changes in the special - // case where cached sizes are required. - - var width = Convert.ToInt32(Bounds.Width); - var height = Convert.ToInt32(Bounds.Height); - - if (width == 0 || height == 0) + var component = ColorComponent; + var isAlphaMaxForced = IsAlphaMaxForced; + var isSaturationValueMaxForced = IsSaturationValueMaxForced; + + if (isAlphaMaxForced && + component != ColorComponent.Alpha) { - // Attempt to use the last size if it was available - if (cachedSize.IsDefault == false) - { - width = Convert.ToInt32(cachedSize.Width); - height = Convert.ToInt32(cachedSize.Height); - } + hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); } - else + + switch (component) { - cachedSize = new Size(width, height); + case ColorComponent.Component1: + return new HsvColor( + hsvColor.A, + hsvColor.H, + isSaturationValueMaxForced ? 1.0 : hsvColor.S, + isSaturationValueMaxForced ? 1.0 : hsvColor.V); + case ColorComponent.Component2: + return new HsvColor( + hsvColor.A, + hsvColor.H, + hsvColor.S, + isSaturationValueMaxForced ? 1.0 : hsvColor.V); + case ColorComponent.Component3: + return new HsvColor( + hsvColor.A, + hsvColor.H, + isSaturationValueMaxForced ? 1.0 : hsvColor.S, + hsvColor.V); + default: + return hsvColor; } + } + + /// + /// Gets the actual background color displayed for the given RGB color. + /// This can differ due to the effects of certain properties intended to improve perception. + /// + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + private Color GetEquivalentBackgroundColor(Color rgbColor) + { + var component = ColorComponent; + var isAlphaMaxForced = IsAlphaMaxForced; - var bitmap = await ColorHelpers.CreateComponentBitmapAsync( - width, - height, - Orientation, - ColorModel, - ColorComponent, - color, - IsAlphaMaxForced, - IsSaturationValueMaxForced); - - if (bitmap != null) + if (isAlphaMaxForced && + component != ColorComponent.Alpha) { - Background = ColorHelpers.BitmapToBrushAsync(bitmap, width, height); + rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); } - return; + return rgbColor; } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - bool update = false; + if (disableUpdates) + { + base.OnPropertyChanged(change); + return; + } + // Always keep the two color properties in sync if (change.Property == ColorProperty) { - // Sync with HSV (which is primary) + disableUpdates = true; + HsvColor = Color.ToHsv(); - update = true; + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + disableUpdates = false; } else if (change.Property == HsvColorProperty) { - update = true; + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + disableUpdates = false; } else if (change.Property == BoundsProperty) { - update = true; + if (IsAutoUpdatingEnabled) + { + UpdateBackground(); + } } - - if (update && IsAutoUpdatingEnabled) + else if (change.Property == ValueProperty || + change.Property == MinimumProperty || + change.Property == MaximumProperty) { - UpdateColors(); + disableUpdates = true; + + (var color, var hsvColor) = GetColorFromSliderValues(); + + if (ColorModel == ColorModel.Hsva) + { + HsvColor = hsvColor; + Color = hsvColor.ToRgb(); + } + else + { + Color = color; + HsvColor = color.ToHsv(); + } + + disableUpdates = false; } base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs index 37c6f552d6..6500d10fe9 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs @@ -719,7 +719,7 @@ namespace Avalonia.Controls.Primitives var brush = new ImageBrush(bitmap) { - Stretch = Stretch.Fill + Stretch = Stretch.None }; return brush; From aae4b6708d15343f5f962e0c7a7c63d626e44064 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 21:57:55 -0400 Subject: [PATCH 11/41] Remove Color property from ColorPreviewer --- .../ColorPreviewer.Properties.cs | 27 +++---------------- .../ColorPreviewer/ColorPreviewer.cs | 18 +------------ 2 files changed, 4 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index 74c0943919..903b5fb52b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -5,27 +5,6 @@ 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. /// @@ -38,9 +17,9 @@ namespace Avalonia.Controls.Primitives /// 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. + /// Only an HSV color is supported in this control to ensure there is never any + /// loss of precision or color information. Accent colors, like the color spectrum, + /// only operate with the HSV color model. /// public HsvColor HsvColor { diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 35bd62601f..1c0dd2154a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives { /// /// Event for when the selected color changes within the previewer. - /// This happens when an accent color is pressed. + /// This occurs when an accent color is pressed. /// public event EventHandler? ColorChanged; @@ -82,22 +82,6 @@ namespace Avalonia.Controls.Primitives 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. /// From 357eddf5e933f4797e21d2235e19936c66878b21 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 26 Apr 2022 22:52:00 -0400 Subject: [PATCH 12/41] Implement ColorSlider PseudoClasses --- .../ColorSlider/ColorSlider.cs | 66 ++++++++++++++++--- .../Themes/Fluent/ColorSlider.xaml | 20 ++++-- .../Themes/Fluent/ColorSpectrum.xaml | 3 + 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 61df36c806..9d9e9f393e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls.Metadata; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Utilities; @@ -8,8 +9,13 @@ namespace Avalonia.Controls.Primitives /// /// A slider with a background that represents a single color component. /// + [PseudoClasses(pcDarkSelector, pcLightSelector)] public partial class ColorSlider : Slider { + protected const string pcDarkSelector = ":dark-selector"; + protected const string pcLightSelector = ":light-selector"; + + private const double MaxHue = 359.99999999999999999; private bool disableUpdates = false; /// @@ -19,6 +25,49 @@ namespace Avalonia.Controls.Primitives { } + /// + /// Updates the visual state of the control by applying latest PseudoClasses. + /// + private void UpdatePseudoClasses() + { + // The slider itself can be transparent for certain color values. + // This causes an issue where a white selector thumb over a light window background or + // a black selector thumb over a dark window background is not visible. + // This means under a certain alpha threshold, neither a white or black selector thumb + // should be shown and instead the default slider thumb color should be used instead. + if (Color.A < 128 && + (IsAlphaMaxForced == false || + ColorComponent == ColorComponent.Alpha)) + { + PseudoClasses.Set(pcDarkSelector, false); + PseudoClasses.Set(pcLightSelector, false); + } + else + { + Color perceivedColor; + + if (ColorModel == ColorModel.Hsva) + { + perceivedColor = GetEquivalentBackgroundColor(HsvColor).ToRgb(); + } + else + { + perceivedColor = GetEquivalentBackgroundColor(Color); + } + + if (ColorHelpers.GetRelativeLuminance(perceivedColor) <= 0.5) + { + PseudoClasses.Set(pcDarkSelector, false); + PseudoClasses.Set(pcLightSelector, true); + } + else + { + PseudoClasses.Set(pcDarkSelector, true); + PseudoClasses.Set(pcLightSelector, false); + } + } + } + /// /// Generates a new background image for the color slider and applies it. /// @@ -80,7 +129,7 @@ namespace Avalonia.Controls.Primitives break; case ColorComponent.Component1: // Hue Minimum = 0; - Maximum = 359; + Maximum = MaxHue; Value = hsvColor.H; break; case ColorComponent.Component2: // Saturation @@ -144,26 +193,22 @@ namespace Avalonia.Controls.Primitives { case ColorComponent.Alpha: { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - hsvColor = new HsvColor(componentValue, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + hsvColor = new HsvColor(sliderPercent, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); break; } case ColorComponent.Component1: { - var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); - hsvColor = new HsvColor(baseHsvColor.A, componentValue, baseHsvColor.S, baseHsvColor.V); + hsvColor = new HsvColor(baseHsvColor.A, sliderPercent * MaxHue, baseHsvColor.S, baseHsvColor.V); break; } case ColorComponent.Component2: { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, componentValue, baseHsvColor.V); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, sliderPercent, baseHsvColor.V); break; } case ColorComponent.Component3: { - var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); - hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, componentValue); + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, sliderPercent); break; } } @@ -279,6 +324,7 @@ namespace Avalonia.Controls.Primitives UpdateBackground(); } + UpdatePseudoClasses(); disableUpdates = false; } else if (change.Property == HsvColorProperty) @@ -293,6 +339,7 @@ namespace Avalonia.Controls.Primitives UpdateBackground(); } + UpdatePseudoClasses(); disableUpdates = false; } else if (change.Property == BoundsProperty) @@ -321,6 +368,7 @@ namespace Avalonia.Controls.Primitives HsvColor = color.ToHsv(); } + UpdatePseudoClasses(); disableUpdates = false; } diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index 1ca9b12ffe..620e9f658d 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -159,14 +159,26 @@ - + + + + + - - + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index b209fe75b3..8e5139975a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -118,6 +118,9 @@ + From 25a34efd9a01cfb86698038cc54c4292ec1947ff Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:08:33 -0400 Subject: [PATCH 13/41] Implement color display name in ColorSpectrum --- .../ColorPreviewer.Properties.cs | 2 +- .../ColorSpectrum/ColorSpectrum.cs | 60 +++++---- .../Helpers/ColorHelpers.cs | 24 +--- .../Helpers/ColorNameHelpers.cs | 116 ++++++++++++++++++ .../Themes/Default/ColorSpectrum.xaml | 16 +-- .../Themes/Fluent/ColorSpectrum.xaml | 16 +-- 6 files changed, 174 insertions(+), 60 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index 903b5fb52b..f90f02551d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -41,7 +41,7 @@ namespace Avalonia.Controls.Primitives /// public bool ShowAccentColors { - get => (bool)this.GetValue(ShowAccentColorsProperty); + get => GetValue(ShowAccentColorsProperty); set => SetValue(ShowAccentColorsProperty, value); } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index fe9a2fac43..9b2459265d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -20,7 +20,6 @@ namespace Avalonia.Controls.Primitives /// /// A two dimensional spectrum for color selection. /// - [TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))] [TemplatePart("PART_InputTarget", typeof(Canvas))] [TemplatePart("PART_LayoutRoot", typeof(Panel))] [TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))] @@ -60,7 +59,6 @@ namespace Avalonia.Controls.Primitives private Ellipse? _spectrumOverlayEllipse; private Canvas? _inputTarget; private Panel? _selectionEllipsePanel; - private ToolTip? _colorNameToolTip; // Put the spectrum images in a bitmap, which is then given to an ImageBrush. private WriteableBitmap? _hueRedBitmap; @@ -117,7 +115,6 @@ namespace Avalonia.Controls.Primitives UnregisterEvents(); // Failsafe - _colorNameToolTip = e.NameScope.Find("PART_ColorNameToolTip"); _inputTarget = e.NameScope.Find("PART_InputTarget"); _layoutRoot = e.NameScope.Find("PART_LayoutRoot"); _selectionEllipsePanel = e.NameScope.Find("PART_SelectionEllipsePanel"); @@ -152,10 +149,10 @@ namespace Avalonia.Controls.Primitives }); } - if (ColorHelpers.ToDisplayNameExists && - _colorNameToolTip != null) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color)); } // If we haven't yet created our bitmaps, do so now. @@ -338,26 +335,45 @@ namespace Avalonia.Controls.Primitives protected override void OnGotFocus(GotFocusEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, true); + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); + + base.OnGotFocus(e); } /// protected override void OnLostFocus(RoutedEventArgs e) { // We only want to bother with the color name tool tip if we can provide color names. - if (_colorNameToolTip != null && - ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) + { + ToolTip.SetIsOpen(_selectionEllipsePanel, false); + } + + UpdatePseudoClasses(); + + base.OnLostFocus(e); + } + + /// + protected override void OnPointerLeave(PointerEventArgs e) + { + // We only want to bother with the color name tool tip if we can provide color names. + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, false); + ToolTip.SetIsOpen(_selectionEllipsePanel, false); } UpdatePseudoClasses(); + + base.OnPointerLeave(e); } /// @@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); ColorChanged?.Invoke(this, colorChangedEventArgs); - if (ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); - } + ToolTip.SetTip(_selectionEllipsePanel, ColorNameHelpers.ToDisplayName(Color)); } } } @@ -811,15 +825,11 @@ namespace Avalonia.Controls.Primitives Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. - if (ColorHelpers.ToDisplayNameExists) + if (IsFocused && + _selectionEllipsePanel != null && + ColorNameHelpers.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - // ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves, - // so toggling IsEnabled induces it to do that without incurring any visual glitches. - _colorNameToolTip.IsEnabled = false; - _colorNameToolTip.IsEnabled = true; - } + ToolTip.SetIsOpen(_selectionEllipsePanel, true); } UpdatePseudoClasses(); diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs index 6500d10fe9..36ee478c7b 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelpers.cs @@ -17,18 +17,6 @@ namespace Avalonia.Controls.Primitives { internal static class ColorHelpers { - public const int CheckerSize = 4; - - public static bool ToDisplayNameExists - { - get => false; - } - - public static string ToDisplayName(Color color) - { - return string.Empty; - } - /// /// Generates a new bitmap of the specified size by changing a specific color component. /// This will produce a gradient representing a sweep of all possible values of the color component. @@ -325,7 +313,7 @@ namespace Avalonia.Controls.Primitives { Hsv newHsv = originalHsv; - if (amount == IncrementAmount.Small || !ToDisplayNameExists) + if (amount == IncrementAmount.Small || !ColorNameHelpers.ToDisplayNameExists) { // In order to avoid working with small values that can incur rounding issues, // we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1. @@ -416,7 +404,7 @@ namespace Avalonia.Controls.Primitives // in the middle of that color's bounds. Hsv newHsv = originalHsv; - string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); + string originalColorName = ColorNameHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); string newColorName = originalColorName; // Note: *newValue replaced with ref local variable for C#, must be initialized @@ -471,7 +459,7 @@ namespace Avalonia.Controls.Primitives { newValue = maxBound; shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); break; } } @@ -486,7 +474,7 @@ namespace Avalonia.Controls.Primitives { newValue = minBound; shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); break; } } @@ -501,7 +489,7 @@ namespace Avalonia.Controls.Primitives break; } - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); + newColorName = ColorNameHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); } if (shouldFindMidPoint) @@ -574,7 +562,7 @@ namespace Avalonia.Controls.Primitives } } - currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); + currentColorName = ColorNameHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); } newValue = (startValue + currentValue + startEndOffset) / 2; diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs new file mode 100644 index 0000000000..303a52f00b --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorNameHelpers.cs @@ -0,0 +1,116 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using Avalonia.Media; +using System.Text; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Contains helpers useful when working with color names. + /// + public static class ColorNameHelpers + { + private static readonly Dictionary cachedDisplayNames = new Dictionary(); + private static readonly object cacheMutex = new object(); + + /// + /// Determines if color display names are supported based on the current thread culture. + /// + /// + /// Only English names are currently supported following known color names. + /// In the future known color names could be localized. + /// + public static bool ToDisplayNameExists + { + get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines an approximate display name for the given color. + /// + /// The color to get the display name for. + /// The approximate color display name. + public static string ToDisplayName(Color color) + { + // Without rounding, there are 16,777,216 possible RGB colors (without alpha). + // This is too many to cache and search through for performance reasons. + // It is also needlessly large as there are only ~140 known/named colors. + // Therefore, rounding of the input color's component values is done to + // reduce the color space into something more useful. + double rounding = 5; + var roundedColor = new Color( + 0xFF, + Convert.ToByte(Math.Round(color.R / rounding) * rounding), + Convert.ToByte(Math.Round(color.G / rounding) * rounding), + Convert.ToByte(Math.Round(color.B / rounding) * rounding)); + + // Attempt to use a previously cached display name + lock (cacheMutex) + { + if (cachedDisplayNames.TryGetValue(roundedColor, out var displayName)) + { + return displayName; + } + } + + // Find the closest known color by measuring 3D Euclidean distance (ignore alpha) + var closestKnownColor = KnownColor.None; + var closestKnownColorDistance = double.PositiveInfinity; + var knownColors = (KnownColor[])Enum.GetValues(typeof(KnownColor)); + + for (int i = 1; i < knownColors.Length; i++) // Skip 'None' + { + // Transparent is skipped since alpha is ignored making it equivalent to White + if (knownColors[i] != KnownColor.Transparent) + { + Color knownColor = KnownColors.ToColor(knownColors[i]); + + double distance = Math.Sqrt( + Math.Pow((double)(roundedColor.R - knownColor.R), 2.0) + + Math.Pow((double)(roundedColor.G - knownColor.G), 2.0) + + Math.Pow((double)(roundedColor.B - knownColor.B), 2.0)); + + if (distance < closestKnownColorDistance) + { + closestKnownColor = knownColors[i]; + closestKnownColorDistance = distance; + } + } + } + + // Return the closest known color as the display name + // Cache results for next time as well + if (closestKnownColor != KnownColor.None) + { + StringBuilder sb = new StringBuilder(); + string name = closestKnownColor.ToString(); + + // Add spaces converting PascalCase to human-readable names + for (int i = 0; i < name.Length; i++) + { + if (i != 0 && + char.IsUpper(name[i])) + { + sb.Append(' '); + } + + sb.Append(name[i]); + } + + string displayName = sb.ToString(); + + lock (cacheMutex) + { + cachedDisplayNames.Add(roundedColor, displayName); + } + + return displayName; + } + else + { + return string.Empty; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 9596ca9653..78e6da8aa3 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -48,7 +48,10 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + - - - - + VerticalAlignment="Stretch" /> + + + - + + - - - - + VerticalAlignment="Stretch" /> + + + Date: Wed, 27 Apr 2022 22:37:36 -0400 Subject: [PATCH 14/41] Follow Avalonia convention --- .../ColorPreviewer/ColorPreviewer.cs | 6 ------ .../ColorSlider/ColorSlider.cs | 4 ---- .../ColorSpectrum/ColorSpectrum.cs | 2 -- 3 files changed, 12 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 1c0dd2154a..35072d6a42 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -61,8 +61,6 @@ namespace Avalonia.Controls.Primitives eventsConnected = false; } - - return; } /// @@ -92,8 +90,6 @@ namespace Avalonia.Controls.Primitives HsvColor = newColor; ColorChanged?.Invoke(this, new ColorChangedEventArgs(oldColor.ToRgb(), newColor.ToRgb())); - - return; } /// @@ -115,8 +111,6 @@ namespace Avalonia.Controls.Primitives HsvColor newHsvColor = AccentColorConverter.GetAccent(hsvColor, accentStep); OnColorChanged(newHsvColor); - - return; } } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 9d9e9f393e..c73f2b1cea 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -100,8 +100,6 @@ namespace Avalonia.Controls.Primitives Background = ColorHelpers.BitmapToBrushAsync(bitmap, pixelWidth, pixelHeight); } } - - return; } /// @@ -170,8 +168,6 @@ namespace Avalonia.Controls.Primitives break; } } - - return; } /// diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 9b2459265d..7b68068d46 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -327,8 +327,6 @@ namespace Avalonia.Controls.Primitives maxBound)); e.Handled = true; - - return; } /// From d9ef01acfcd9d7d504558ca7ac92941351a94cb3 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:40:26 -0400 Subject: [PATCH 15/41] Render the ColorSpectrum to physical device pixel resolution --- .../ColorSpectrum/ColorSpectrum.cs | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 7b68068d46..4cabb5c5b7 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; @@ -587,8 +588,10 @@ namespace Avalonia.Controls.Primitives return; } - double xPosition = point.Position.X; - double yPosition = point.Position.Y; + // Remember the bitmap size follows physical device pixels + var scale = LayoutHelper.GetLayoutScale(this); + double xPosition = point.Position.X * scale; + double yPosition = point.Position.Y * scale; double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2; double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2)); @@ -819,8 +822,10 @@ namespace Avalonia.Controls.Primitives yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius; } - Canvas.SetLeft(_selectionEllipsePanel, xPosition - (_selectionEllipsePanel.Width / 2)); - Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2)); + // Remember the bitmap size follows physical device pixels + var scale = LayoutHelper.GetLayoutScale(this); + Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2)); + Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2)); // We only want to bother with the color name tool tip if we can provide color names. if (IsFocused && @@ -969,7 +974,14 @@ namespace Avalonia.Controls.Primitives List bgraMaxPixelData = new List(); List newHsvValues = new List(); - var pixelCount = (int)(Math.Round(minDimension) * Math.Round(minDimension)); + // In Avalonia, Bounds returns the actual device-independent pixel size of a control. + // However, this is not necessarily the size of the control rendered on a display. + // A desktop or application scaling factor may be applied which must be accounted for here. + // Remember bitmaps in Avalonia are rendered mapping to actual device pixels, not the device- + // independent pixels of controls. + var scale = LayoutHelper.GetLayoutScale(this); + int pixelDimension = (int)Math.Round(minDimension * scale); + var pixelCount = pixelDimension * pixelDimension; var pixelDataSize = pixelCount * 4; bgraMinPixelData.Capacity = pixelDataSize; @@ -986,8 +998,6 @@ namespace Avalonia.Controls.Primitives bgraMaxPixelData.Capacity = pixelDataSize; newHsvValues.Capacity = pixelCount; - int minDimensionInt = (int)Math.Round(minDimension); - await Task.Run(() => { // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes, @@ -1006,12 +1016,12 @@ namespace Avalonia.Controls.Primitives // but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap. if (shape == ColorSpectrumShape.Box) { - for (int x = minDimensionInt - 1; x >= 0; --x) + for (int x = pixelDimension - 1; x >= 0; --x) { - for (int y = minDimensionInt - 1; y >= 0; --y) + for (int y = pixelDimension - 1; y >= 0; --y) { FillPixelForBox( - x, y, hsv, minDimensionInt, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, hsv, pixelDimension, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1019,12 +1029,12 @@ namespace Avalonia.Controls.Primitives } else { - for (int y = 0; y < minDimensionInt; ++y) + for (int y = 0; y < pixelDimension; ++y) { - for (int x = 0; x < minDimensionInt; ++x) + for (int x = 0; x < pixelDimension; ++x) { FillPixelForRing( - x, y, minDimensionInt / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, + x, y, pixelDimension / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue, bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData, newHsvValues); } @@ -1034,8 +1044,8 @@ namespace Avalonia.Controls.Primitives Dispatcher.UIThread.Post(() => { - int pixelWidth = (int)Math.Round(minDimension); - int pixelHeight = (int)Math.Round(minDimension); + int pixelWidth = pixelDimension; + int pixelHeight = pixelDimension; ColorSpectrumComponents components2 = Components; @@ -1066,8 +1076,8 @@ namespace Avalonia.Controls.Primitives _shapeFromLastBitmapCreation = Shape; _componentsFromLastBitmapCreation = Components; - _imageWidthFromLastBitmapCreation = minDimension; - _imageHeightFromLastBitmapCreation = minDimension; + _imageWidthFromLastBitmapCreation = pixelDimension; + _imageHeightFromLastBitmapCreation = pixelDimension; _minHueFromLastBitmapCreation = MinHue; _maxHueFromLastBitmapCreation = MaxHue; _minSaturationFromLastBitmapCreation = MinSaturation; @@ -1086,7 +1096,7 @@ namespace Avalonia.Controls.Primitives double x, double y, Hsv baseHsv, - double minDimension, + int minDimension, ColorSpectrumComponents components, double minHue, double maxHue, From aef0d012258da94978d96ff38ce19af1c6554090 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:47:29 -0400 Subject: [PATCH 16/41] Reorder properties following Avalonia convention --- .../ColorPreviewer.Properties.cs | 16 +-- .../ColorSlider/ColorSlider.Properties.cs | 96 ++++++------- .../ColorSpectrum/ColorSpectrum.Properties.cs | 136 +++++++++--------- 3 files changed, 124 insertions(+), 124 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs index f90f02551d..c545f25298 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -13,6 +13,14 @@ namespace Avalonia.Controls.Primitives nameof(HsvColor), Colors.Transparent.ToHsv()); + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowAccentColorsProperty = + AvaloniaProperty.Register( + nameof(ShowAccentColors), + true); + /// /// Gets or sets the currently previewed color in the HSV color model. /// @@ -27,14 +35,6 @@ namespace Avalonia.Controls.Primitives 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. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs index 3aa3e3a789..12dce0b03e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -13,6 +13,54 @@ namespace Avalonia.Controls.Primitives nameof(Color), Colors.White); + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorComponentProperty = + AvaloniaProperty.Register( + nameof(ColorComponent), + ColorComponent.Component1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorModelProperty = + AvaloniaProperty.Register( + nameof(ColorModel), + ColorModel.Rgba); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv()); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAlphaMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsAlphaMaxForced), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsAutoUpdatingEnabledProperty = + AvaloniaProperty.Register( + nameof(IsAutoUpdatingEnabled), + true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsSaturationValueMaxForcedProperty = + AvaloniaProperty.Register( + nameof(IsSaturationValueMaxForced), + true); + /// /// Gets or sets the currently selected color in the RGB color model. /// @@ -26,14 +74,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorComponentProperty = - AvaloniaProperty.Register( - nameof(ColorComponent), - ColorComponent.Component1); - /// /// Gets or sets the color component represented by the slider. /// @@ -43,14 +83,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorComponentProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorModelProperty = - AvaloniaProperty.Register( - nameof(ColorModel), - ColorModel.Rgba); - /// /// Gets or sets the active color model used by the slider. /// @@ -60,14 +92,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorModelProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register( - nameof(HsvColor), - Colors.White.ToHsv()); - /// /// Gets or sets the currently selected color in the HSV color model. /// @@ -81,14 +105,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(HsvColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty IsAlphaMaxForcedProperty = - AvaloniaProperty.Register( - nameof(IsAlphaMaxForced), - true); - /// /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components /// other than . @@ -100,14 +116,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAlphaMaxForcedProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty IsAutoUpdatingEnabledProperty = - AvaloniaProperty.Register( - nameof(IsAutoUpdatingEnabled), - true); - /// /// Gets or sets a value indicating whether automatic background and foreground updates will be /// calculated when the set color changes. @@ -121,14 +129,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(IsAutoUpdatingEnabledProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty IsSaturationValueMaxForcedProperty = - AvaloniaProperty.Register( - nameof(IsSaturationValueMaxForced), - true); - /// /// Gets or sets a value indicating whether the saturation and value components are always forced to maximum values /// when using the HSVA color model. Only component values other than will be changed. diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index ab5b83afcb..a1cb43a95a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -10,6 +10,74 @@ namespace Avalonia.Controls.Primitives /// public partial class ColorSpectrum { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ComponentsProperty = + AvaloniaProperty.Register( + nameof(Components), + ColorSpectrumComponents.HueSaturation); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.White.ToHsv()); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxHueProperty = + AvaloniaProperty.Register(nameof(MaxHue), 359); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxSaturationProperty = + AvaloniaProperty.Register(nameof(MaxSaturation), 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaxValueProperty = + AvaloniaProperty.Register(nameof(MaxValue), 100); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinHueProperty = + AvaloniaProperty.Register(nameof(MinHue), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinSaturationProperty = + AvaloniaProperty.Register(nameof(MinSaturation), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinValueProperty = + AvaloniaProperty.Register(nameof(MinValue), 0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShapeProperty = + AvaloniaProperty.Register( + nameof(Shape), + ColorSpectrumShape.Box); + /// /// Gets or sets the currently selected color in the RGB color model. /// @@ -23,14 +91,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorProperty = - AvaloniaProperty.Register( - nameof(Color), - Colors.White); - /// /// Gets or sets the two HSV color components displayed by the spectrum. /// @@ -43,14 +103,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(ComponentsProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ComponentsProperty = - AvaloniaProperty.Register( - nameof(Components), - ColorSpectrumComponents.HueSaturation); - /// /// Gets or sets the currently selected color in the HSV color model. /// @@ -65,14 +117,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(HsvColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register( - nameof(HsvColor), - Colors.White.ToHsv()); - /// /// Gets or sets the maximum value of the Hue component in the range from 0..359. /// This property must be greater than . @@ -86,12 +130,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxHueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxHueProperty = - AvaloniaProperty.Register(nameof(MaxHue), 359); - /// /// Gets or sets the maximum value of the Saturation component in the range from 0..100. /// This property must be greater than . @@ -105,12 +143,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxSaturationProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxSaturationProperty = - AvaloniaProperty.Register(nameof(MaxSaturation), 100); - /// /// Gets or sets the maximum value of the Value component in the range from 0..100. /// This property must be greater than . @@ -124,12 +156,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MaxValueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MaxValueProperty = - AvaloniaProperty.Register(nameof(MaxValue), 100); - /// /// Gets or sets the minimum value of the Hue component in the range from 0..359. /// This property must be less than . @@ -143,12 +169,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinHueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinHueProperty = - AvaloniaProperty.Register(nameof(MinHue), 0); - /// /// Gets or sets the minimum value of the Saturation component in the range from 0..100. /// This property must be less than . @@ -162,12 +182,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinSaturationProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinSaturationProperty = - AvaloniaProperty.Register(nameof(MinSaturation), 0); - /// /// Gets or sets the minimum value of the Value component in the range from 0..100. /// This property must be less than . @@ -181,12 +195,6 @@ namespace Avalonia.Controls.Primitives set => SetValue(MinValueProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty MinValueProperty = - AvaloniaProperty.Register(nameof(MinValue), 0); - /// /// Gets or sets the displayed shape of the spectrum. /// @@ -195,13 +203,5 @@ namespace Avalonia.Controls.Primitives get => GetValue(ShapeProperty); set => SetValue(ShapeProperty, value); } - - /// - /// Defines the property. - /// - public static readonly StyledProperty ShapeProperty = - AvaloniaProperty.Register( - nameof(Shape), - ColorSpectrumShape.Box); } } From 21190ff39bba3fcb242cf3225247b36d74b3d082 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 22:53:16 -0400 Subject: [PATCH 17/41] Add :dark-selector PseudoClass to ColorSpectrum This standardizes with ColorSlider (which requires three states) but so far isn't needed in the templates. --- .../ColorSpectrum/ColorSpectrum.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 4cabb5c5b7..563fa24c08 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -29,10 +29,11 @@ namespace Avalonia.Controls.Primitives [TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))] [TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))] [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))] - [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)] + [PseudoClasses(pcPressed, pcLargeSelector, pcDarkSelector, pcLightSelector)] public partial class ColorSpectrum : TemplatedControl { protected const string pcPressed = ":pressed"; + protected const string pcDarkSelector = ":dark-selector"; protected const string pcLargeSelector = ":large-selector"; protected const string pcLightSelector = ":light-selector"; @@ -556,7 +557,16 @@ namespace Avalonia.Controls.Primitives PseudoClasses.Set(pcLargeSelector, false); } - PseudoClasses.Set(pcLightSelector, SelectionEllipseShouldBeLight()); + if (SelectionEllipseShouldBeLight()) + { + PseudoClasses.Set(pcDarkSelector, false); + PseudoClasses.Set(pcLightSelector, true); + } + else + { + PseudoClasses.Set(pcDarkSelector, true); + PseudoClasses.Set(pcLightSelector, false); + } } private void UpdateColor(Hsv newHsv) From c3cdf856a3345f74413aed31febb61d4f214eb21 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:14:29 -0400 Subject: [PATCH 18/41] Add default themes for ColorSlider and ColorPreviewer --- .../Themes/Default.xaml | 21 ++ .../Themes/Default/ColorPreviewer.xaml | 90 ++++++++ .../Themes/Default/ColorSlider.xaml | 198 ++++++++++++++++++ .../Themes/Fluent/ColorSlider.xaml | 32 ++- 4 files changed, 332 insertions(+), 9 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml create mode 100644 src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml index 528eed9969..6d2f979f6e 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml @@ -1,7 +1,28 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml new file mode 100644 index 0000000000..fe35dbd587 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -0,0 +1,90 @@ + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml new file mode 100644 index 0000000000..c0b78d628a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml index 620e9f658d..54f58d2a8f 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -8,6 +8,20 @@ + + - - - - - - - From a86e0cc64e18787cae2ebb7b531966ee8b3ec0dd Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:19:02 -0400 Subject: [PATCH 19/41] Move ColorPicker theme definitions into theme folders --- samples/ControlCatalog/App.xaml.cs | 4 ++-- .../Themes/{ => Default}/Default.xaml | 0 .../Themes/{ => Fluent}/Fluent.xaml | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/Avalonia.Controls.ColorPicker/Themes/{ => Default}/Default.xaml (100%) rename src/Avalonia.Controls.ColorPicker/Themes/{ => Fluent}/Fluent.xaml (100%) diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6539cdaee6..7ebb87094a 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -20,12 +20,12 @@ namespace ControlCatalog public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml") }; public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) { - Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml") + Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml") }; public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles")) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml similarity index 100% rename from src/Avalonia.Controls.ColorPicker/Themes/Default.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml similarity index 100% rename from src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml From e0bc2e35c50e96563e9dd0f24cc33160cf8765be Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:25:11 -0400 Subject: [PATCH 20/41] Add RgbComponent enum and support direct casting with all component enums --- .../ColorComponent.cs | 8 ++-- .../HsvComponent.cs | 22 +++++----- .../RgbComponent.cs | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/RgbComponent.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs index a0385c03b4..71725056cf 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs @@ -8,21 +8,21 @@ /// /// Represents the alpha component. /// - Alpha, + Alpha = 0, /// /// Represents the first color component which is Red when RGB or Hue when HSV. /// - Component1, + Component1 = 1, /// /// Represents the second color component which is Green when RGB or Saturation when HSV. /// - Component2, + Component2 = 2, /// /// Represents the third color component which is Blue when RGB or Value when HSV. /// - Component3 + Component3 = 3 } } diff --git a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs index 1132bd7bbb..1a7a13166a 100644 --- a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs +++ b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs @@ -12,13 +12,21 @@ namespace Avalonia.Controls /// public enum HsvComponent { + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha = 0, + /// /// The Hue component. /// /// /// Also see: /// - Hue, + Hue = 1, /// /// The Saturation component. @@ -26,7 +34,7 @@ namespace Avalonia.Controls /// /// Also see: /// - Saturation, + Saturation = 2, /// /// The Value component. @@ -34,14 +42,6 @@ namespace Avalonia.Controls /// /// Also see: /// - Value, - - /// - /// The Alpha component. - /// - /// - /// Also see: - /// - Alpha + Value = 3 }; } diff --git a/src/Avalonia.Controls.ColorPicker/RgbComponent.cs b/src/Avalonia.Controls.ColorPicker/RgbComponent.cs new file mode 100644 index 0000000000..c3591573bb --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/RgbComponent.cs @@ -0,0 +1,42 @@ +using Avalonia.Media; + +namespace Avalonia.Controls +{ + /// + /// Defines a specific component in the RGB color model. + /// + public enum RgbComponent + { + /// + /// The Alpha component. + /// + /// + /// Also see: + /// + Alpha = 0, + + /// + /// The Red component. + /// + /// + /// Also see: + /// + Red = 1, + + /// + /// The Green component. + /// + /// + /// Also see: + /// + Green = 2, + + /// + /// The Blue component. + /// + /// + /// Also see: + /// + Blue = 3 + }; +} From c3ce137bda4483c51d2cca1b79c761ca326f6be8 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:27:05 -0400 Subject: [PATCH 21/41] Move AccentColorConverter in Converters directory --- .../{ColorPreviewer => Converters}/AccentColorConverter.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Avalonia.Controls.ColorPicker/{ColorPreviewer => Converters}/AccentColorConverter.cs (100%) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs similarity index 100% rename from src/Avalonia.Controls.ColorPicker/ColorPreviewer/AccentColorConverter.cs rename to src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs From f550b8f9e82fa0f02490316a2e0d07840d73b0b4 Mon Sep 17 00:00:00 2001 From: robloo Date: Wed, 27 Apr 2022 23:30:22 -0400 Subject: [PATCH 22/41] Move AccentColorConverter in Avalonia.Controls.Primitives.Converters namespace This better hides these special-purpose converters --- .../ColorPreviewer/ColorPreviewer.cs | 1 + .../Converters/AccentColorConverter.cs | 2 +- .../Themes/Default/ColorPreviewer.xaml | 4 ++-- .../Themes/Fluent/ColorPreviewer.xaml | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs index 35072d6a42..3c429783d5 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives.Converters; using Avalonia.Input; using Avalonia.Media; diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs index ad8f66251a..07ebc899db 100644 --- a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -3,7 +3,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; -namespace Avalonia.Controls.Primitives +namespace Avalonia.Controls.Primitives.Converters { /// /// Creates an accent color for a given base color value and step parameter. diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml index fe35dbd587..9100bf0440 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -1,10 +1,10 @@  - + - - - - diff --git a/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs new file mode 100644 index 0000000000..9b7fcecf45 --- /dev/null +++ b/src/Avalonia.Themes.Default/IBitmapToImageConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media.Imaging; + +namespace Avalonia.Themes.Default +{ + internal class IBitmapToImageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value != null && value is IBitmap bm) + return new Image { Source=bm }; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 35603fe216..ede0791438 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -10,6 +10,15 @@ + + + + + + + MSBuild:Compile + + diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 7860e08ef5..6251c86720 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -4,12 +4,13 @@ x:CompileBindings="True" Selector="NativeMenuBar"> - + + diff --git a/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs new file mode 100644 index 0000000000..34670882f8 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Media.Imaging; + +namespace Avalonia.Themes.Fluent +{ + internal class IBitmapToImageConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value != null && value is IBitmap bm) + return new Image { Source=bm }; + + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} From bb8aaee1e0da5116d06febbe8aa512add3be0ce0 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 2 May 2022 23:09:13 +0200 Subject: [PATCH 39/41] Optimizing resource related code. --- .../Controls/ResourceNodeExtensions.cs | 11 ++++------- src/Avalonia.Base/Styling/IStyle.cs | 2 +- src/Avalonia.Base/Styling/Styles.cs | 2 +- .../Controls/DataValidationErrors.xaml | 10 +++++++--- .../MarkupExtensions/StaticResourceExtension.cs | 17 +++++++---------- .../Styling/StyleInclude.cs | 6 +++--- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index 513b3f2424..1758c45650 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -40,19 +40,16 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceHost? current = control; + IResourceNode? current = control; while (current != null) { - if (current is IResourceHost host) + if (current.TryGetResource(key, out value)) { - if (host.TryGetResource(key, out value)) - { - return true; - } + return true; } - current = (current as IStyledElement)?.StylingParent as IResourceHost; + current = (current as IStyledElement)?.StylingParent as IResourceNode; } value = null; diff --git a/src/Avalonia.Base/Styling/IStyle.cs b/src/Avalonia.Base/Styling/IStyle.cs index 78fbe0f2b5..738a69cb88 100644 --- a/src/Avalonia.Base/Styling/IStyle.cs +++ b/src/Avalonia.Base/Styling/IStyle.cs @@ -8,7 +8,7 @@ namespace Avalonia.Styling /// /// Defines the interface for styles. /// - public interface IStyle + public interface IStyle : IResourceNode { /// /// Gets a collection of child styles. diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 81502f1570..d79081152e 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -160,7 +160,7 @@ namespace Avalonia.Styling for (var i = Count - 1; i >= 0; --i) { - if (this[i] is IResourceProvider p && p.TryGetResource(key, out value)) + if (this[i].TryGetResource(key, out value)) { return true; } diff --git a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml index a3a4cf4662..d7bf4bbbf1 100644 --- a/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml @@ -1,9 +1,13 @@ - + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index db33b88cc3..f865f87220 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions targetType = setter.Property.PropertyType; } - // Look upwards though the ambient context for IResourceHosts and IResourceProviders + // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. - foreach (var e in stack.Parents) + foreach (var parent in stack.Parents) { - object value; - - if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value)) - { - return ColorToBrushConverter.Convert(value, targetType); - } - else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value)) + if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value)) { return ColorToBrushConverter.Convert(value, targetType); } @@ -58,7 +52,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { - DelayedBinding.Add(target, property, x => GetValue(x, targetType)); + var localTargetType = targetType; + var localInstance = this; + + DelayedBinding.Add(target, property, x => localInstance.GetValue(x, localTargetType)); return AvaloniaProperty.UnsetValue; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 607b552c28..fa4a27fc50 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.Styling } } - bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; + bool IResourceNode.HasResources => Loaded?.HasResources ?? false; IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); @@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling public bool TryGetResource(object key, out object? value) { - if (!_isLoading && Loaded is IResourceProvider p) + if (!_isLoading) { - return p.TryGetResource(key, out value); + return Loaded.TryGetResource(key, out value); } value = null; From 4aa0f878c2890dca2ade446832192dafae5d0675 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 2 May 2022 23:25:18 +0200 Subject: [PATCH 40/41] Add an explanation why certain locals are copied. --- .../MarkupExtensions/StaticResourceExtension.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index f865f87220..add97a660b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -52,6 +52,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (provideTarget.TargetObject is IControl target && provideTarget.TargetProperty is PropertyInfo property) { + // This is stored locally to avoid allocating closure in the outer scope. var localTargetType = targetType; var localInstance = this; From 6a8eb5a1cf551503c2090e3a0b8dfdc4eec8497c Mon Sep 17 00:00:00 2001 From: peter kuhn Date: Wed, 4 May 2022 06:58:39 +0200 Subject: [PATCH 41/41] Fix Typo --- src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj | 9 --------- src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index ede0791438..35603fe216 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -10,15 +10,6 @@ - - - - - - - MSBuild:Compile - - diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 6251c86720..d40ba0cc1d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -5,7 +5,7 @@ Selector="NativeMenuBar"> - + @@ -21,7 +21,7 @@ - +