diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 6539cdaee6c..7ebb87094ae 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -20,12 +20,12 @@ public App() 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/samples/ControlCatalog/Pages/ColorPickerPage.xaml b/samples/ControlCatalog/Pages/ColorPickerPage.xaml index ec34193f8c6..c0c83d6a351 100644 --- a/samples/ControlCatalog/Pages/ColorPickerPage.xaml +++ b/samples/ControlCatalog/Pages/ColorPickerPage.xaml @@ -3,27 +3,77 @@ 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" + xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker" + mc:Ignorable="d" + d:DesignWidth="800" + d:DesignHeight="450" x:Class="ControlCatalog.Pages.ColorPickerPage"> - - + + + + + + + + + + + + + + - + + - + ColorComponent="Alpha" + ColorModel="Hsva" + Orientation="Vertical" + HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" /> + + diff --git a/src/Avalonia.Controls.ColorPicker/ColorComponent.cs b/src/Avalonia.Controls.ColorPicker/ColorComponent.cs new file mode 100644 index 00000000000..71725056cf6 --- /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 = 0, + + /// + /// Represents the first color component which is Red when RGB or Hue when HSV. + /// + Component1 = 1, + + /// + /// Represents the second color component which is Green when RGB or Saturation when HSV. + /// + Component2 = 2, + + /// + /// Represents the third color component which is Blue when RGB or Value when HSV. + /// + Component3 = 3 + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorModel.cs b/src/Avalonia.Controls.ColorPicker/ColorModel.cs new file mode 100644 index 00000000000..f11b514706f --- /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/ColorPreviewer/ColorPreviewer.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs new file mode 100644 index 00000000000..0fa6ab80835 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs @@ -0,0 +1,50 @@ +using Avalonia.Data; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives +{ + /// + public partial class ColorPreviewer + { + /// + /// Defines the property. + /// + public static readonly StyledProperty HsvColorProperty = + AvaloniaProperty.Register( + nameof(HsvColor), + Colors.Transparent.ToHsv(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// 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. + /// + /// + /// 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 + { + get => GetValue(HsvColorProperty); + set => SetValue(HsvColorProperty, value); + } + + /// + /// Gets or sets a value indicating whether accent colors are shown along + /// with the preview color. + /// + public bool ShowAccentColors + { + get => 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 00000000000..d04ddf4bd6b --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs @@ -0,0 +1,130 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives.Converters; +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 occurs 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; + } + } + + /// + 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) + { + if (change.Property == HsvColorProperty) + { + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().ToRgb())); + } + + base.OnPropertyChanged(change); + } + + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } + + /// + /// 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); + HsvColor oldHsvColor = HsvColor; + + HsvColor = newHsvColor; + OnColorChanged(new ColorChangedEventArgs(oldHsvColor.ToRgb(), newHsvColor.ToRgb())); + } + } +} 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 00000000000..31bd296288f --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs @@ -0,0 +1,146 @@ +using Avalonia.Data; +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, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// 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(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// 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. + /// + /// + /// 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); + } + + /// + /// Gets or sets the color component represented by the slider. + /// + public ColorComponent ColorComponent + { + get => GetValue(ColorComponentProperty); + set => SetValue(ColorComponentProperty, value); + } + + /// + /// Gets or sets the active color model used by the slider. + /// + public ColorModel ColorModel + { + get => GetValue(ColorModelProperty); + set => SetValue(ColorModelProperty, value); + } + + /// + /// 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); + } + + /// + /// 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); + } + + /// + /// 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); + } + + /// + /// 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 00000000000..3c38c6ed1b6 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -0,0 +1,399 @@ +using System; +using Avalonia.Controls.Metadata; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Utilities; + +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"; + + /// + /// Event for when the selected color changes within the slider. + /// + public event EventHandler? ColorChanged; + + private const double MaxHue = 359.99999999999999999; // 17 decimal places + private bool disableUpdates = false; + + /// + /// Initializes a new instance of the class. + /// + public ColorSlider() : base() + { + } + + /// + /// 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 (ColorHelper.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. + /// + private async void UpdateBackground() + { + // 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); + var pixelWidth = Convert.ToInt32(Bounds.Width * scale); + var pixelHeight = Convert.ToInt32(Bounds.Height * scale); + + if (pixelWidth != 0 && pixelHeight != 0) + { + var bitmap = await ColorPickerHelpers.CreateComponentBitmapAsync( + pixelWidth, + pixelHeight, + Orientation, + ColorModel, + ColorComponent, + HsvColor, + IsAlphaMaxForced, + IsSaturationValueMaxForced); + + if (bitmap != null) + { + Background = new ImageBrush(ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight)); + } + } + } + + /// + /// 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) + { + // Note: Components converted into a usable range for the user + switch (component) + { + case ColorComponent.Alpha: + Minimum = 0; + Maximum = 100; + Value = hsvColor.A * 100; + break; + case ColorComponent.Component1: // Hue + Minimum = 0; + Maximum = MaxHue; + 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.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; + } + } + } + + /// + /// 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); + + var baseHsvColor = HsvColor; + var baseRgbColor = Color; + var component = ColorComponent; + + if (ColorModel == ColorModel.Hsva) + { + switch (component) + { + case ColorComponent.Alpha: + { + hsvColor = new HsvColor(sliderPercent, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component1: + { + hsvColor = new HsvColor(baseHsvColor.A, sliderPercent * MaxHue, baseHsvColor.S, baseHsvColor.V); + break; + } + case ColorComponent.Component2: + { + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, sliderPercent, baseHsvColor.V); + break; + } + case ColorComponent.Component3: + { + hsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, sliderPercent); + break; + } + } + + return (hsvColor.ToRgb(), hsvColor); + } + else + { + 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(baseRgbColor.A, componentValue, baseRgbColor.G, baseRgbColor.B); + break; + case ColorComponent.Component2: + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, componentValue, baseRgbColor.B); + break; + case ColorComponent.Component3: + rgbColor = new Color(baseRgbColor.A, baseRgbColor.R, baseRgbColor.G, componentValue); + break; + } + + return (rgbColor, rgbColor.ToHsv()); + } + } + + /// + /// 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. + /// + /// The actual color to get the equivalent background color for. + /// The equivalent, perceived background color. + private HsvColor GetEquivalentBackgroundColor(HsvColor hsvColor) + { + var component = ColorComponent; + var isAlphaMaxForced = IsAlphaMaxForced; + var isSaturationValueMaxForced = IsSaturationValueMaxForced; + + if (isAlphaMaxForced && + component != ColorComponent.Alpha) + { + hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); + } + + switch (component) + { + 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; + + if (isAlphaMaxForced && + component != ColorComponent.Alpha) + { + rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); + } + + return rgbColor; + } + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (disableUpdates) + { + base.OnPropertyChanged(change); + return; + } + + // Always keep the two color properties in sync + if (change.Property == ColorProperty) + { + disableUpdates = true; + + HsvColor = Color.ToHsv(); + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue(), + change.GetNewValue())); + + disableUpdates = false; + } + else if (change.Property == HsvColorProperty) + { + disableUpdates = true; + + Color = HsvColor.ToRgb(); + + if (IsAutoUpdatingEnabled) + { + SetColorToSliderValues(); + UpdateBackground(); + } + + UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs( + change.GetOldValue().ToRgb(), + change.GetNewValue().ToRgb())); + + disableUpdates = false; + } + else if (change.Property == BoundsProperty) + { + if (IsAutoUpdatingEnabled) + { + UpdateBackground(); + } + } + else if (change.Property == ValueProperty || + change.Property == MinimumProperty || + change.Property == MaximumProperty) + { + disableUpdates = true; + + Color oldColor = Color; + (var color, var hsvColor) = GetColorFromSliderValues(); + + if (ColorModel == ColorModel.Hsva) + { + HsvColor = hsvColor; + Color = hsvColor.ToRgb(); + } + else + { + Color = color; + HsvColor = color.ToHsv(); + } + + UpdatePseudoClasses(); + OnColorChanged(new ColorChangedEventArgs(oldColor, Color)); + + disableUpdates = false; + } + + base.OnPropertyChanged(change); + } + + /// + /// Called before the event occurs. + /// + /// The defining old/new colors. + protected virtual void OnColorChanged(ColorChangedEventArgs e) + { + ColorChanged?.Invoke(this, e); + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs deleted file mode 100644 index b912d39aba0..00000000000 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs +++ /dev/null @@ -1,414 +0,0 @@ -// This source file is adapted from the WinUI project. -// (https://github.com/microsoft/microsoft-ui-xaml) -// -// Licensed to The Avalonia Project under the MIT License. - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Platform; - -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; - } - - public static Hsv IncrementColorComponent( - Hsv originalHsv, - HsvComponent component, - IncrementDirection direction, - IncrementAmount amount, - bool shouldWrap, - double minBound, - double maxBound) - { - Hsv newHsv = originalHsv; - - if (amount == IncrementAmount.Small || !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. - newHsv.S *= 100; - newHsv.V *= 100; - - // Note: *valueToIncrement replaced with ref local variable for C#, must be initialized - ref double valueToIncrement = ref newHsv.H; - double incrementAmount = 0.0; - - // If we're adding a small increment, then we'll just add or subtract 1. - // If we're adding a large increment, then we want to snap to the next - // or previous major value - for hue, this is every increment of 30; - // for saturation and value, this is every increment of 10. - switch (component) - { - case HsvComponent.Hue: - valueToIncrement = ref newHsv.H; - incrementAmount = amount == IncrementAmount.Small ? 1 : 30; - break; - - case HsvComponent.Saturation: - valueToIncrement = ref newHsv.S; - incrementAmount = amount == IncrementAmount.Small ? 1 : 10; - break; - - case HsvComponent.Value: - valueToIncrement = ref newHsv.V; - incrementAmount = amount == IncrementAmount.Small ? 1 : 10; - break; - - default: - throw new InvalidOperationException("Invalid HsvComponent."); - } - - double previousValue = valueToIncrement; - - valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount); - - // If the value has reached outside the bounds, we were previous at the boundary, and we should wrap, - // then we'll place the selection on the other side of the spectrum. - // Otherwise, we'll place it on the boundary that was exceeded. - if (valueToIncrement < minBound) - { - valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound; - } - - if (valueToIncrement > maxBound) - { - valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound; - } - - // We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range. - newHsv.S /= 100; - newHsv.V /= 100; - } - else - { - // While working with named colors, we're going to need to be working in actual HSV units, - // so we'll divide the min bound and max bound by 100 in the case of saturation or value, - // since we'll have received units between 0-100 and we need them within 0-1. - if (component == HsvComponent.Saturation || - component == HsvComponent.Value) - { - minBound /= 100; - maxBound /= 100; - } - - newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound); - } - - return newHsv; - } - - public static Hsv FindNextNamedColor( - Hsv originalHsv, - HsvComponent component, - IncrementDirection direction, - bool shouldWrap, - double minBound, - double maxBound) - { - // There's no easy way to directly get the next named color, so what we'll do - // is just iterate in the direction that we want to find it until we find a color - // in that direction that has a color name different than our current color name. - // Once we find a new color name, then we'll iterate across that color name until - // we find its bounds on the other side, and then select the color that is exactly - // in the middle of that color's bounds. - Hsv newHsv = originalHsv; - - string originalColorName = ColorHelpers.ToDisplayName(originalHsv.ToRgb().ToColor()); - string newColorName = originalColorName; - - // Note: *newValue replaced with ref local variable for C#, must be initialized - double originalValue = 0.0; - ref double newValue = ref newHsv.H; - double incrementAmount = 0.0; - - switch (component) - { - case HsvComponent.Hue: - originalValue = originalHsv.H; - newValue = ref newHsv.H; - incrementAmount = 1; - break; - - case HsvComponent.Saturation: - originalValue = originalHsv.S; - newValue = ref newHsv.S; - incrementAmount = 0.01; - break; - - case HsvComponent.Value: - originalValue = originalHsv.V; - newValue = ref newHsv.V; - incrementAmount = 0.01; - break; - - default: - throw new InvalidOperationException("Invalid HsvComponent."); - } - - bool shouldFindMidPoint = true; - - while (newColorName == originalColorName) - { - double previousValue = newValue; - newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; - - bool justWrapped = false; - - // If we've hit a boundary, then either we should wrap or we shouldn't. - // If we should, then we'll perform that wrapping if we were previously up against - // the boundary that we've now hit. Otherwise, we'll stop at that boundary. - if (newValue > maxBound) - { - if (shouldWrap) - { - newValue = minBound; - justWrapped = true; - } - else - { - newValue = maxBound; - shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); - break; - } - } - else if (newValue < minBound) - { - if (shouldWrap) - { - newValue = maxBound; - justWrapped = true; - } - else - { - newValue = minBound; - shouldFindMidPoint = false; - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); - break; - } - } - - if (!justWrapped && - previousValue != originalValue && - Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue)) - { - // If we've wrapped all the way back to the start and have failed to find a new color name, - // then we'll just quit - there isn't a new color name that we're going to find. - shouldFindMidPoint = false; - break; - } - - newColorName = ColorHelpers.ToDisplayName(newHsv.ToRgb().ToColor()); - } - - if (shouldFindMidPoint) - { - Hsv startHsv = newHsv; - Hsv currentHsv = startHsv; - double startEndOffset = 0; - string currentColorName = newColorName; - - // Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized - ref double startValue = ref startHsv.H; - ref double currentValue = ref currentHsv.H; - double wrapIncrement = 0; - - switch (component) - { - case HsvComponent.Hue: - startValue = ref startHsv.H; - currentValue = ref currentHsv.H; - wrapIncrement = 360.0; - break; - - case HsvComponent.Saturation: - startValue = ref startHsv.S; - currentValue = ref currentHsv.S; - wrapIncrement = 1.0; - break; - - case HsvComponent.Value: - startValue = ref startHsv.V; - currentValue = ref currentHsv.V; - wrapIncrement = 1.0; - break; - - default: - throw new InvalidOperationException("Invalid HsvComponent."); - } - - while (newColorName == currentColorName) - { - currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; - - // If we've hit a boundary, then either we should wrap or we shouldn't. - // If we should, then we'll perform that wrapping if we were previously up against - // the boundary that we've now hit. Otherwise, we'll stop at that boundary. - if (currentValue > maxBound) - { - if (shouldWrap) - { - currentValue = minBound; - startEndOffset = maxBound - minBound; - } - else - { - currentValue = maxBound; - break; - } - } - else if (currentValue < minBound) - { - if (shouldWrap) - { - currentValue = maxBound; - startEndOffset = minBound - maxBound; - } - else - { - currentValue = minBound; - break; - } - } - - currentColorName = ColorHelpers.ToDisplayName(currentHsv.ToRgb().ToColor()); - } - - newValue = (startValue + currentValue + startEndOffset) / 2; - - // Dividing by 2 may have gotten us halfway through a single step, so we'll - // remove that half-step if it exists. - double leftoverValue = Math.Abs(newValue); - - while (leftoverValue > incrementAmount) - { - leftoverValue -= incrementAmount; - } - - newValue -= leftoverValue; - - while (newValue < minBound) - { - newValue += wrapIncrement; - } - - while (newValue > maxBound) - { - newValue -= wrapIncrement; - } - } - - return newHsv; - } - - public static double IncrementAlphaComponent( - double originalAlpha, - IncrementDirection direction, - IncrementAmount amount, - bool shouldWrap, - double minBound, - double maxBound) - { - // In order to avoid working with small values that can incur rounding issues, - // we'll multiple alpha by 100 to put it in the range of 0-100 instead of 0-1. - originalAlpha *= 100; - - const double smallIncrementAmount = 1; - const double largeIncrementAmount = 10; - - if (amount == IncrementAmount.Small) - { - originalAlpha += (direction == IncrementDirection.Lower ? -1 : 1) * smallIncrementAmount; - } - else - { - if (direction == IncrementDirection.Lower) - { - originalAlpha = Math.Ceiling((originalAlpha - largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; - } - else - { - originalAlpha = Math.Floor((originalAlpha + largeIncrementAmount) / largeIncrementAmount) * largeIncrementAmount; - } - } - - // If the value has reached outside the bounds and we should wrap, then we'll place the selection - // on the other side of the spectrum. Otherwise, we'll place it on the boundary that was exceeded. - if (originalAlpha < minBound) - { - originalAlpha = shouldWrap ? maxBound : minBound; - } - - if (originalAlpha > maxBound) - { - originalAlpha = shouldWrap ? minBound : maxBound; - } - - // We multiplied alpha by 100 previously, so now we want to put it back in the 0-1 range. - return originalAlpha / 100; - } - - public static WriteableBitmap CreateBitmapFromPixelData( - int pixelWidth, - int pixelHeight, - List bgraPixelData) - { - Vector dpi = new Vector(96, 96); // Standard may need to change on some devices - - WriteableBitmap 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.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); - } - - return bitmap; - } - - /// - /// Gets the relative (perceptual) luminance/brightness of the given color. - /// 1 is closer to white while 0 is closer to black. - /// - /// The color to calculate relative luminance for. - /// The relative (perceptual) luminance/brightness of the given color. - public static double GetRelativeLuminance(Color color) - { - // The equation for relative luminance is given by - // - // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg - // - // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise } - // - // If L is closer to 1, then the color is closer to white; if it is closer to 0, - // then the color is closer to black. This is based on the fact that the human - // eye perceives green to be much brighter than red, which in turn is perceived to be - // brighter than blue. - - double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4); - double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4); - double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4); - - return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg); - } - } -} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs index 824bf9ab053..587a89ee38a 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs @@ -3,6 +3,7 @@ // // Licensed to The Avalonia Project under the MIT License. +using Avalonia.Data; using Avalonia.Media; namespace Avalonia.Controls.Primitives @@ -10,6 +11,88 @@ namespace Avalonia.Controls.Primitives /// public partial class ColorSpectrum { + /// + /// Defines the property. + /// + public static readonly StyledProperty ColorProperty = + AvaloniaProperty.Register( + nameof(Color), + Colors.White, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// 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(), + defaultBindingMode: BindingMode.TwoWay); + + /// + /// 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 +106,6 @@ public Color Color set => SetValue(ColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty ColorProperty = - AvaloniaProperty.Register( - nameof(Color), - Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); - /// /// Gets or sets the two HSV color components displayed by the spectrum. /// @@ -43,14 +118,6 @@ public ColorSpectrumComponents Components 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 +132,6 @@ public HsvColor HsvColor set => SetValue(HsvColorProperty, value); } - /// - /// Defines the property. - /// - public static readonly StyledProperty HsvColorProperty = - AvaloniaProperty.Register( - nameof(HsvColor), - new HsvColor(1, 0, 0, 1)); - /// /// Gets or sets the maximum value of the Hue component in the range from 0..359. /// This property must be greater than . @@ -86,12 +145,6 @@ public int MaxHue 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 +158,6 @@ public int MaxSaturation 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 +171,6 @@ public int MaxValue 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 +184,6 @@ public int MinHue 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 +197,6 @@ public int MinSaturation 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 +210,6 @@ public int MinValue 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 +218,5 @@ public ColorSpectrumShape Shape get => GetValue(ShapeProperty); set => SetValue(ShapeProperty, value); } - - /// - /// Defines the property. - /// - public static readonly StyledProperty ShapeProperty = - AvaloniaProperty.Register( - nameof(Shape), - ColorSpectrumShape.Box); } } diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index fe9a2fac43d..245592207e6 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Shapes; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; @@ -20,7 +21,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))] @@ -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"; @@ -60,7 +61,6 @@ public partial class ColorSpectrum : TemplatedControl 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 +117,6 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 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 +151,10 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) }); } - if (ColorHelpers.ToDisplayNameExists && - _colorNameToolTip != null) + if (_selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(Color); + ToolTip.SetTip(_selectionEllipsePanel, ColorHelper.ToDisplayName(Color)); } // If we haven't yet created our bitmaps, do so now. @@ -320,7 +319,7 @@ protected override void OnKeyDown(KeyEventArgs e) IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small; HsvColor hsvColor = HsvColor; - UpdateColor(ColorHelpers.IncrementColorComponent( + UpdateColor(ColorPickerHelpers.IncrementColorComponent( new Hsv(hsvColor), incrementComponent, direction, @@ -330,34 +329,51 @@ protected override void OnKeyDown(KeyEventArgs e) maxBound)); e.Handled = true; - - return; } /// 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 && + ColorHelper.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 && + ColorHelper.ToDisplayNameExists) { - ToolTip.SetIsOpen(_colorNameToolTip, false); + 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 && + ColorHelper.ToDisplayNameExists) + { + ToolTip.SetIsOpen(_selectionEllipsePanel, false); + } + + UpdatePseudoClasses(); + + base.OnPointerLeave(e); } /// @@ -516,12 +532,10 @@ public void RaiseColorChanged() var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor); ColorChanged?.Invoke(this, colorChangedEventArgs); - if (ColorHelpers.ToDisplayNameExists) + if (_selectionEllipsePanel != null && + ColorHelper.ToDisplayNameExists) { - if (_colorNameToolTip != null) - { - _colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor); - } + ToolTip.SetTip(_selectionEllipsePanel, ColorHelper.ToDisplayName(Color)); } } } @@ -543,7 +557,16 @@ private void UpdatePseudoClasses() 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) @@ -575,8 +598,10 @@ private void UpdateColorFromPoint(PointerPoint point) 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)); @@ -807,19 +832,17 @@ private void UpdateEllipse() 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 (ColorHelpers.ToDisplayNameExists) + if (IsFocused && + _selectionEllipsePanel != null && + ColorHelper.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(); @@ -961,7 +984,14 @@ private async void CreateBitmapsAndColorMap() 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; @@ -978,8 +1008,6 @@ private async void CreateBitmapsAndColorMap() 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, @@ -998,12 +1026,12 @@ await Task.Run(() => // 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); } @@ -1011,12 +1039,12 @@ await Task.Run(() => } 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); } @@ -1026,13 +1054,13 @@ await Task.Run(() => Dispatcher.UIThread.Post(() => { - int pixelWidth = (int)Math.Round(minDimension); - int pixelHeight = (int)Math.Round(minDimension); + int pixelWidth = pixelDimension; + int pixelHeight = pixelDimension; ColorSpectrumComponents components2 = Components; - WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData); - WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData); + WriteableBitmap minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); + WriteableBitmap maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); switch (components2) { @@ -1048,18 +1076,18 @@ await Task.Run(() => case ColorSpectrumComponents.ValueSaturation: case ColorSpectrumComponents.SaturationValue: _hueRedBitmap = minBitmap; - _hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData); - _hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData); - _hueCyanBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle3PixelData); - _hueBlueBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle4PixelData); + _hueYellowBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle1PixelData, pixelWidth, pixelHeight); + _hueGreenBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle2PixelData, pixelWidth, pixelHeight); + _hueCyanBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle3PixelData, pixelWidth, pixelHeight); + _hueBlueBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle4PixelData, pixelWidth, pixelHeight); _huePurpleBitmap = maxBitmap; break; } _shapeFromLastBitmapCreation = Shape; _componentsFromLastBitmapCreation = Components; - _imageWidthFromLastBitmapCreation = minDimension; - _imageHeightFromLastBitmapCreation = minDimension; + _imageWidthFromLastBitmapCreation = pixelDimension; + _imageHeightFromLastBitmapCreation = pixelDimension; _minHueFromLastBitmapCreation = MinHue; _maxHueFromLastBitmapCreation = MaxHue; _minSaturationFromLastBitmapCreation = MinSaturation; @@ -1078,7 +1106,7 @@ private void FillPixelForBox( double x, double y, Hsv baseHsv, - double minDimension, + int minDimension, ColorSpectrumComponents components, double minHue, double maxHue, @@ -1570,7 +1598,7 @@ private bool SelectionEllipseShouldBeLight() displayedColor = Color; } - var lum = ColorHelpers.GetRelativeLuminance(displayedColor); + var lum = ColorHelper.GetRelativeLuminance(displayedColor); return lum <= 0.5; } 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 diff --git a/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs new file mode 100644 index 00000000000..4d05222e315 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs @@ -0,0 +1,116 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Creates an accent color for a given base color value and step parameter. + /// This is a highly-specialized converter for the color picker. + /// + public class AccentColorConverter : IValueConverter + { + /// + /// The amount to change the Value component 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; + + if (value is Color valueColor) + { + rgbColor = valueColor; + } + else if (value is HslColor valueHslColor) + { + rgbColor = valueHslColor.ToRgb(); + } + 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/Converters/ColorToDisplayNameConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs new file mode 100644 index 00000000000..4f727287ba5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs @@ -0,0 +1,68 @@ +using System; +using System.Globalization; +using Avalonia.Controls.Primitives; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Converters +{ + /// + /// Gets the approximated display name for the color. + /// + public class ColorToDisplayNameConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + Color color; + + if (value is Color valueColor) + { + color = valueColor; + } + else if (value is HslColor valueHslColor) + { + color = valueHslColor.ToRgb(); + } + else if (value is HsvColor valueHsvColor) + { + color = valueHsvColor.ToRgb(); + } + else if (value is SolidColorBrush valueBrush) + { + color = valueBrush.Color; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + // ColorHelper.ToDisplayName ignores the alpha component + // This means fully transparent colors will be named as a real color + // That undesirable behavior is specially overridden here + if (color.A == 0x00) + { + return AvaloniaProperty.UnsetValue; + } + else + { + return ColorHelper.ToDisplayName(color); + } + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs new file mode 100644 index 00000000000..9b09073d9dc --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs @@ -0,0 +1,82 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts a color to a hex string and vice versa. + /// + public class ColorToHexConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + Color color; + bool includeSymbol = parameter as bool? ?? false; + + if (value is Color valueColor) + { + color = valueColor; + } + else if (value is HslColor valueHslColor) + { + color = valueHslColor.ToRgb(); + } + else if (value is HsvColor valueHsvColor) + { + color = valueHsvColor.ToRgb(); + } + else if (value is SolidColorBrush valueBrush) + { + color = valueBrush.Color; + } + else + { + // Invalid color value provided + return AvaloniaProperty.UnsetValue; + } + + string hexColor = color.ToString(); + + if (includeSymbol == false) + { + // TODO: When .net standard 2.0 is dropped, replace the below line + //hexColor = hexColor.Replace("#", string.Empty, StringComparison.Ordinal); + hexColor = hexColor.Replace("#", string.Empty); + } + + return hexColor; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + string hexValue = value?.ToString() ?? string.Empty; + + if (Color.TryParse(hexValue, out Color color)) + { + return color; + } + else if (hexValue.StartsWith("#", StringComparison.Ordinal) == false && + Color.TryParse("#" + hexValue, out Color color2)) + { + return color2; + } + else + { + // Invalid hex color value provided + return AvaloniaProperty.UnsetValue; + } + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs new file mode 100644 index 00000000000..220a993f99d --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Gets the third corresponding with a given + /// that represents the other two components. + /// This is a highly-specialized converter for the color picker. + /// + public class ThirdComponentConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value is ColorSpectrumComponents components) + { + // Note: Alpha is not relevant here + switch (components) + { + case ColorSpectrumComponents.HueSaturation: + case ColorSpectrumComponents.SaturationHue: + return (ColorComponent)HsvComponent.Value; + case ColorSpectrumComponents.HueValue: + case ColorSpectrumComponents.ValueHue: + return (ColorComponent)HsvComponent.Saturation; + case ColorSpectrumComponents.SaturationValue: + case ColorSpectrumComponents.ValueSaturation: + return (ColorComponent)HsvComponent.Hue; + } + } + + return AvaloniaProperty.UnsetValue; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs new file mode 100644 index 00000000000..9e8c264dd30 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs @@ -0,0 +1,50 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts the given value into an when a conversion is possible. + /// + public class ToBrushConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value is IBrush brush) + { + return brush; + } + else if (value is Color valueColor) + { + return new SolidColorBrush(valueColor); + } + else if (value is HslColor valueHslColor) + { + return new SolidColorBrush(valueHslColor.ToRgb()); + } + else if (value is HsvColor valueHsvColor) + { + return new SolidColorBrush(valueHsvColor.ToRgb()); + } + + return AvaloniaProperty.UnsetValue; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs b/src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs new file mode 100644 index 00000000000..14b2be225e8 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs @@ -0,0 +1,58 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Converters +{ + /// + /// Converts the given value into a when a conversion is possible. + /// + public class ToColorConverter : IValueConverter + { + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + if (value is Color valueColor) + { + return valueColor; + } + else if (value is HslColor valueHslColor) + { + return valueHslColor.ToRgb(); + } + else if (value is HsvColor valueHsvColor) + { + return valueHsvColor.ToRgb(); + } + else if (value is SolidColorBrush valueBrush) + { + // A brush may have an opacity set along with alpha transparency + double alpha = valueBrush.Color.A * valueBrush.Opacity; + + return new Color( + (byte)MathUtilities.Clamp(alpha, 0x00, 0xFF), + valueBrush.Color.R, + valueBrush.Color.G, + valueBrush.Color.B); + } + + return AvaloniaProperty.UnsetValue; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + return AvaloniaProperty.UnsetValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs new file mode 100644 index 00000000000..2710c220f41 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Avalonia.Controls.Primitives.Converters +{ + /// + /// Converter to chain together multiple converters. + /// + public class ValueConverterGroup : List, IValueConverter + { + /// + /// + public object? Convert( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + object? curValue; + + curValue = value; + for (int i = 0; i < Count; i++) + { + curValue = this[i].Convert(curValue, targetType, parameter, culture); + } + + return curValue; + } + + /// + public object? ConvertBack( + object? value, + Type targetType, + object? parameter, + CultureInfo culture) + { + object? curValue; + + curValue = value; + for (int i = (Count - 1); i >= 0; i--) + { + curValue = this[i].ConvertBack(curValue, targetType, parameter, culture); + } + + return curValue; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs new file mode 100644 index 00000000000..32a898ee715 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -0,0 +1,142 @@ +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 colors. + /// + public static class ColorHelper + { + private static readonly Dictionary cachedDisplayNames = new Dictionary(); + private static readonly object cacheMutex = new object(); + + /// + /// Gets the relative (perceptual) luminance/brightness of the given color. + /// 1 is closer to white while 0 is closer to black. + /// + /// The color to calculate relative luminance for. + /// The relative (perceptual) luminance/brightness of the given color. + public static double GetRelativeLuminance(Color color) + { + // The equation for relative luminance is given by + // + // L = 0.2126 * Rg + 0.7152 * Gg + 0.0722 * Bg + // + // where Xg = { X/3294 if X <= 10, (R/269 + 0.0513)^2.4 otherwise } + // + // If L is closer to 1, then the color is closer to white; if it is closer to 0, + // then the color is closer to black. This is based on the fact that the human + // eye perceives green to be much brighter than red, which in turn is perceived to be + // brighter than blue. + + double rg = color.R <= 10 ? color.R / 3294.0 : Math.Pow(color.R / 269.0 + 0.0513, 2.4); + double gg = color.G <= 10 ? color.G / 3294.0 : Math.Pow(color.G / 269.0 + 0.0513, 2.4); + double bg = color.B <= 10 ? color.B / 3294.0 : Math.Pow(color.B / 269.0 + 0.0513, 2.4); + + return (0.2126 * rg + 0.7152 * gg + 0.0722 * bg); + } + + /// + /// 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/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs new file mode 100644 index 00000000000..c1904a3c305 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -0,0 +1,629 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +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 +{ + /// + /// Contains internal, special-purpose helpers used with the color picker. + /// + internal static class ColorPickerHelpers + { + /// + /// 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. + public 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, + IncrementDirection direction, + IncrementAmount amount, + bool shouldWrap, + double minBound, + double maxBound) + { + Hsv newHsv = originalHsv; + + if (amount == IncrementAmount.Small || !ColorHelper.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. + newHsv.S *= 100; + newHsv.V *= 100; + + // Note: *valueToIncrement replaced with ref local variable for C#, must be initialized + ref double valueToIncrement = ref newHsv.H; + double incrementAmount = 0.0; + + // If we're adding a small increment, then we'll just add or subtract 1. + // If we're adding a large increment, then we want to snap to the next + // or previous major value - for hue, this is every increment of 30; + // for saturation and value, this is every increment of 10. + switch (component) + { + case HsvComponent.Hue: + valueToIncrement = ref newHsv.H; + incrementAmount = amount == IncrementAmount.Small ? 1 : 30; + break; + + case HsvComponent.Saturation: + valueToIncrement = ref newHsv.S; + incrementAmount = amount == IncrementAmount.Small ? 1 : 10; + break; + + case HsvComponent.Value: + valueToIncrement = ref newHsv.V; + incrementAmount = amount == IncrementAmount.Small ? 1 : 10; + break; + + default: + throw new InvalidOperationException("Invalid HsvComponent."); + } + + double previousValue = valueToIncrement; + + valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount); + + // If the value has reached outside the bounds, we were previous at the boundary, and we should wrap, + // then we'll place the selection on the other side of the spectrum. + // Otherwise, we'll place it on the boundary that was exceeded. + if (valueToIncrement < minBound) + { + valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound; + } + + if (valueToIncrement > maxBound) + { + valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound; + } + + // We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range. + newHsv.S /= 100; + newHsv.V /= 100; + } + else + { + // While working with named colors, we're going to need to be working in actual HSV units, + // so we'll divide the min bound and max bound by 100 in the case of saturation or value, + // since we'll have received units between 0-100 and we need them within 0-1. + if (component == HsvComponent.Saturation || + component == HsvComponent.Value) + { + minBound /= 100; + maxBound /= 100; + } + + newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound); + } + + return newHsv; + } + + public static Hsv FindNextNamedColor( + Hsv originalHsv, + HsvComponent component, + IncrementDirection direction, + bool shouldWrap, + double minBound, + double maxBound) + { + // There's no easy way to directly get the next named color, so what we'll do + // is just iterate in the direction that we want to find it until we find a color + // in that direction that has a color name different than our current color name. + // Once we find a new color name, then we'll iterate across that color name until + // we find its bounds on the other side, and then select the color that is exactly + // in the middle of that color's bounds. + Hsv newHsv = originalHsv; + + string originalColorName = ColorHelper.ToDisplayName(originalHsv.ToRgb().ToColor()); + string newColorName = originalColorName; + + // Note: *newValue replaced with ref local variable for C#, must be initialized + double originalValue = 0.0; + ref double newValue = ref newHsv.H; + double incrementAmount = 0.0; + + switch (component) + { + case HsvComponent.Hue: + originalValue = originalHsv.H; + newValue = ref newHsv.H; + incrementAmount = 1; + break; + + case HsvComponent.Saturation: + originalValue = originalHsv.S; + newValue = ref newHsv.S; + incrementAmount = 0.01; + break; + + case HsvComponent.Value: + originalValue = originalHsv.V; + newValue = ref newHsv.V; + incrementAmount = 0.01; + break; + + default: + throw new InvalidOperationException("Invalid HsvComponent."); + } + + bool shouldFindMidPoint = true; + + while (newColorName == originalColorName) + { + double previousValue = newValue; + newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; + + bool justWrapped = false; + + // If we've hit a boundary, then either we should wrap or we shouldn't. + // If we should, then we'll perform that wrapping if we were previously up against + // the boundary that we've now hit. Otherwise, we'll stop at that boundary. + if (newValue > maxBound) + { + if (shouldWrap) + { + newValue = minBound; + justWrapped = true; + } + else + { + newValue = maxBound; + shouldFindMidPoint = false; + newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); + break; + } + } + else if (newValue < minBound) + { + if (shouldWrap) + { + newValue = maxBound; + justWrapped = true; + } + else + { + newValue = minBound; + shouldFindMidPoint = false; + newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); + break; + } + } + + if (!justWrapped && + previousValue != originalValue && + Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue)) + { + // If we've wrapped all the way back to the start and have failed to find a new color name, + // then we'll just quit - there isn't a new color name that we're going to find. + shouldFindMidPoint = false; + break; + } + + newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); + } + + if (shouldFindMidPoint) + { + Hsv startHsv = newHsv; + Hsv currentHsv = startHsv; + double startEndOffset = 0; + string currentColorName = newColorName; + + // Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized + ref double startValue = ref startHsv.H; + ref double currentValue = ref currentHsv.H; + double wrapIncrement = 0; + + switch (component) + { + case HsvComponent.Hue: + startValue = ref startHsv.H; + currentValue = ref currentHsv.H; + wrapIncrement = 360.0; + break; + + case HsvComponent.Saturation: + startValue = ref startHsv.S; + currentValue = ref currentHsv.S; + wrapIncrement = 1.0; + break; + + case HsvComponent.Value: + startValue = ref startHsv.V; + currentValue = ref currentHsv.V; + wrapIncrement = 1.0; + break; + + default: + throw new InvalidOperationException("Invalid HsvComponent."); + } + + while (newColorName == currentColorName) + { + currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; + + // If we've hit a boundary, then either we should wrap or we shouldn't. + // If we should, then we'll perform that wrapping if we were previously up against + // the boundary that we've now hit. Otherwise, we'll stop at that boundary. + if (currentValue > maxBound) + { + if (shouldWrap) + { + currentValue = minBound; + startEndOffset = maxBound - minBound; + } + else + { + currentValue = maxBound; + break; + } + } + else if (currentValue < minBound) + { + if (shouldWrap) + { + currentValue = maxBound; + startEndOffset = minBound - maxBound; + } + else + { + currentValue = minBound; + break; + } + } + + currentColorName = ColorHelper.ToDisplayName(currentHsv.ToRgb().ToColor()); + } + + newValue = (startValue + currentValue + startEndOffset) / 2; + + // Dividing by 2 may have gotten us halfway through a single step, so we'll + // remove that half-step if it exists. + double leftoverValue = Math.Abs(newValue); + + while (leftoverValue > incrementAmount) + { + leftoverValue -= incrementAmount; + } + + newValue -= leftoverValue; + + while (newValue < minBound) + { + newValue += wrapIncrement; + } + + while (newValue > maxBound) + { + newValue -= wrapIncrement; + } + } + + return newHsv; + } + + /// + /// Converts the given raw BGRA pre-multiplied alpha pixel data into a bitmap. + /// + /// The bitmap (in raw BGRA pre-multiplied alpha pixels). + /// The pixel width of the bitmap. + /// The pixel height of the bitmap. + /// A new . + public static WriteableBitmap CreateBitmapFromPixelData( + IList bgraPixelData, + int pixelWidth, + int pixelHeight) + { + // 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.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + } + + return bitmap; + } + } +} 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 diff --git a/src/Avalonia.Controls.ColorPicker/HsvComponent.cs b/src/Avalonia.Controls.ColorPicker/HsvComponent.cs index 1132bd7bbba..1a7a13166a7 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 @@ public enum HsvComponent /// /// Also see: /// - Saturation, + Saturation = 2, /// /// The Value component. @@ -34,14 +42,6 @@ public enum HsvComponent /// /// 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 00000000000..c3591573bbc --- /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 + }; +} 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 00000000000..15e5ca16554 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + 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 00000000000..19f10201a5a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml similarity index 91% rename from src/Avalonia.Controls.ColorPicker/Themes/Default.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml index 832daf8853c..78e6da8aa34 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Default.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml @@ -1,7 +1,7 @@ - + xmlns:converters="using:Avalonia.Controls.Converters" + x:CompileBindings="True"> @@ -48,7 +48,10 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + - - - - + VerticalAlignment="Stretch" /> + + + + + + + + + + + + + + + + + + + + + + + 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 00000000000..cb764a738cb --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + 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 00000000000..18a081721a5 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml similarity index 89% rename from src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml rename to src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml index 545702ea844..ac8e2a9c061 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml @@ -1,7 +1,7 @@ - + xmlns:converters="using:Avalonia.Controls.Converters" + x:CompileBindings="True"> @@ -48,7 +48,10 @@ Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - + + - - - - + VerticalAlignment="Stretch" /> + + + + diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml new file mode 100644 index 00000000000..c25d79727fc --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs b/src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs index b2433bfd979..a91f1430195 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 @@ public class CornerRadiusFilterConverter : IValueConverter /// 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 @@ public class CornerRadiusFilterConverter : IValueConverter 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(); }