Browse Source

Merge branch 'master' into Megapit_NativeMenuBar_Icons

Peter Kuhn 3 years ago
parent
commit
8f5eaf2483
45 changed files with 2985 additions and 620 deletions
  1. 2 2
      samples/ControlCatalog/App.xaml.cs
  2. 68 18
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  3. 4 7
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  4. 1 1
      src/Avalonia.Base/Styling/IStyle.cs
  5. 1 1
      src/Avalonia.Base/Styling/Styles.cs
  6. 1 1
      src/Avalonia.Base/Utilities/WeakHashList.cs
  7. 28 0
      src/Avalonia.Controls.ColorPicker/ColorComponent.cs
  8. 18 0
      src/Avalonia.Controls.ColorPicker/ColorModel.cs
  9. 50 0
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
  10. 130 0
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  11. 146 0
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  12. 399 0
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  13. 0 414
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs
  14. 83 68
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  15. 83 55
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  16. 0 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs
  17. 0 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs
  18. 116 0
      src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
  19. 68 0
      src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs
  20. 82 0
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  21. 51 0
      src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
  22. 50 0
      src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs
  23. 58 0
      src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs
  24. 50 0
      src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs
  25. 142 0
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  26. 629 0
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  27. 0 0
      src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs
  28. 0 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs
  29. 0 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs
  30. 0 0
      src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs
  31. 11 11
      src/Avalonia.Controls.ColorPicker/HsvComponent.cs
  32. 42 0
      src/Avalonia.Controls.ColorPicker/RgbComponent.cs
  33. 86 0
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  34. 194 0
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  35. 11 11
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  36. 28 0
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  37. 86 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  38. 194 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
  39. 14 11
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml
  40. 28 0
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
  41. 12 3
      src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs
  42. 1 1
      src/Avalonia.Controls/Viewbox.cs
  43. 7 3
      src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
  44. 8 10
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  45. 3 3
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

+ 2 - 2
samples/ControlCatalog/App.xaml.cs

@@ -20,12 +20,12 @@ namespace ControlCatalog
 
 
         public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
         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"))
         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"))
         public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))

+ 68 - 18
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@@ -3,27 +3,77 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls"
              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">
              x:Class="ControlCatalog.Pages.ColorPickerPage">
 
 
-  <Grid ColumnDefinitions="Auto,Auto"
-        RowDefinitions="Auto,Auto">
-    <ColorSpectrum Grid.Column="0"
+  <UserControl.Resources>
+    <pc:ThirdComponentConverter x:Key="ThirdComponent" />
+  </UserControl.Resources>
+
+  <Grid ColumnDefinitions="Auto,10,Auto">
+    <Grid Grid.Column="0"
+          Grid.Row="0"
+          RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
+      <ColorSpectrum x:Name="ColorSpectrum1"
+                     Grid.Row="0"
+                     Color="Red"
+                     CornerRadius="10"
+                     Height="256"
+                     Width="256" />
+      <ColorSlider Grid.Row="1"
+                   Margin="0,10,0,0"
+                   ColorComponent="Component1"
+                   ColorModel="Hsva"
+                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
+      <ColorSlider Grid.Row="2"
+                   ColorComponent="Component2"
+                   ColorModel="Hsva"
+                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
+      <ColorSlider Grid.Row="3"
+                   ColorComponent="Component3"
+                   ColorModel="Hsva"
+                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
+      <ColorSlider Grid.Row="4"
+                   ColorComponent="Alpha"
+                   ColorModel="Hsva"
+                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
+      <ColorPreviewer Grid.Row="5"
+                      ShowAccentColors="True"
+                      HsvColor="{Binding HsvColor, ElementName=ColorSpectrum1}" />
+    </Grid>
+    <Grid Grid.Column="2"
+          Grid.Row="0"
+          ColumnDefinitions="Auto,Auto,Auto"
+          RowDefinitions="Auto,Auto">
+      <ColorSlider Grid.Column="0"
                    Grid.Row="0"
                    Grid.Row="0"
-                   Color="Red"
-                   Height="256"
-                   Width="256" />
-    <ColorSpectrum Grid.Column="1"
+                   IsAlphaMaxForced="True"
+                   IsSaturationValueMaxForced="False"
+                   ColorComponent="{Binding Components, ElementName=ColorSpectrum2, Converter={StaticResource ThirdComponent}}"
+                   ColorModel="Hsva"
+                   Orientation="Vertical"
+                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
+      <ColorSpectrum x:Name="ColorSpectrum2"
+                     Grid.Column="1"
+                     Grid.Row="0"
+                     Color="Green"
+                     Shape="Ring"
+                     Height="256"
+                     Width="256" />
+      <ColorSlider Grid.Column="2"
                    Grid.Row="0"
                    Grid.Row="0"
-                   Color="Green"
-                   Shape="Ring"
-                   Height="256"
-                   Width="256" />
-    <ColorSpectrum Grid.Column="0"
-                   Grid.Row="1"
-                   CornerRadius="10"
-                   Color="Blue"
-                   Height="256"
-                   Width="256" />
+                   ColorComponent="Alpha"
+                   ColorModel="Hsva"
+                   Orientation="Vertical"
+                   HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
+      <ColorPreviewer Grid.Column="0"
+                      Grid.ColumnSpan="3"
+                      Grid.Row="1"
+                      ShowAccentColors="True"
+                      HsvColor="{Binding HsvColor, ElementName=ColorSpectrum2}" />
+    </Grid>
   </Grid>
   </Grid>
 </UserControl>
 </UserControl>

+ 4 - 7
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

@@ -40,19 +40,16 @@ namespace Avalonia.Controls
             control = control ?? throw new ArgumentNullException(nameof(control));
             control = control ?? throw new ArgumentNullException(nameof(control));
             key = key ?? throw new ArgumentNullException(nameof(key));
             key = key ?? throw new ArgumentNullException(nameof(key));
 
 
-            IResourceHost? current = control;
+            IResourceNode? current = control;
 
 
             while (current != null)
             while (current != null)
             {
             {
-                if (current is IResourceHost host)
+                if (current.TryGetResource(key, out value))
                 {
                 {
-                    if (host.TryGetResource(key, out value))
-                    {
-                        return true;
-                    }
+                    return true;
                 }
                 }
 
 
-                current = (current as IStyledElement)?.StylingParent as IResourceHost;
+                current = (current as IStyledElement)?.StylingParent as IResourceNode;
             }
             }
 
 
             value = null;
             value = null;

+ 1 - 1
src/Avalonia.Base/Styling/IStyle.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Styling
     /// <summary>
     /// <summary>
     /// Defines the interface for styles.
     /// Defines the interface for styles.
     /// </summary>
     /// </summary>
-    public interface IStyle
+    public interface IStyle : IResourceNode
     {
     {
         /// <summary>
         /// <summary>
         /// Gets a collection of child styles.
         /// Gets a collection of child styles.

+ 1 - 1
src/Avalonia.Base/Styling/Styles.cs

@@ -160,7 +160,7 @@ namespace Avalonia.Styling
 
 
             for (var i = Count - 1; i >= 0; --i)
             for (var i = Count - 1; i >= 0; --i)
             {
             {
-                if (this[i] is IResourceProvider p && p.TryGetResource(key, out value))
+                if (this[i].TryGetResource(key, out value))
                 {
                 {
                     return true;
                     return true;
                 }
                 }

+ 1 - 1
src/Avalonia.Base/Utilities/WeakHashList.cs

@@ -118,7 +118,7 @@ internal class WeakHashList<T> where T : class
     {
     {
         if (_arr != null)
         if (_arr != null)
         {
         {
-            for (var c = 0; c < _arr.Length; c++)
+            for (var c = 0; c < _arrCount; c++)
             {
             {
                 if (_arr[c]?.TryGetTarget(out var target) == true && target == item)
                 if (_arr[c]?.TryGetTarget(out var target) == true && target == item)
                 {
                 {

+ 28 - 0
src/Avalonia.Controls.ColorPicker/ColorComponent.cs

@@ -0,0 +1,28 @@
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Defines a specific component within a color model.
+    /// </summary>
+    public enum ColorComponent
+    {
+        /// <summary>
+        /// Represents the alpha component.
+        /// </summary>
+        Alpha = 0,
+
+        /// <summary>
+        /// Represents the first color component which is Red when RGB or Hue when HSV.
+        /// </summary>
+        Component1 = 1,
+
+        /// <summary>
+        /// Represents the second color component which is Green when RGB or Saturation when HSV.
+        /// </summary>
+        Component2 = 2,
+
+        /// <summary>
+        /// Represents the third color component which is Blue when RGB or Value when HSV.
+        /// </summary>
+        Component3 = 3
+    }
+}

+ 18 - 0
src/Avalonia.Controls.ColorPicker/ColorModel.cs

@@ -0,0 +1,18 @@
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Defines the model used to represent colors.
+    /// </summary>
+    public enum ColorModel
+    {
+        /// <summary>
+        /// Color is represented by hue, saturation, value and alpha components.
+        /// </summary>
+        Hsva,
+
+        /// <summary>
+        /// Color is represented by red, green, blue and alpha components.
+        /// </summary>
+        Rgba
+    }
+}

+ 50 - 0
src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs

@@ -0,0 +1,50 @@
+using Avalonia.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <inheritdoc/>
+    public partial class ColorPreviewer
+    {
+        /// <summary>
+        /// Defines the <see cref="HsvColor"/> property.
+        /// </summary>
+        public static readonly StyledProperty<HsvColor> HsvColorProperty =
+            AvaloniaProperty.Register<ColorPreviewer, HsvColor>(
+                nameof(HsvColor),
+                Colors.Transparent.ToHsv(),
+                defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="ShowAccentColors"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> ShowAccentColorsProperty =
+            AvaloniaProperty.Register<ColorPreviewer, bool>(
+                nameof(ShowAccentColors),
+                true);
+
+        /// <summary>
+        /// Gets or sets the currently previewed color in the HSV color model.
+        /// </summary>
+        /// <remarks>
+        /// 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.
+        /// </remarks>
+        public HsvColor HsvColor
+        {
+            get => GetValue(HsvColorProperty);
+            set => SetValue(HsvColorProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether accent colors are shown along
+        /// with the preview color.
+        /// </summary>
+        public bool ShowAccentColors
+        {
+            get => GetValue(ShowAccentColorsProperty);
+            set => SetValue(ShowAccentColorsProperty, value);
+        }
+    }
+}

+ 130 - 0
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
+{
+    /// <summary>
+    /// Presents a preview color with optional accent colors.
+    /// </summary>
+    [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
+    {
+        /// <summary>
+        /// Event for when the selected color changes within the previewer.
+        /// This occurs when an accent color is pressed.
+        /// </summary>
+        public event EventHandler<ColorChangedEventArgs>? ColorChanged;
+
+        private bool eventsConnected = false;
+
+        private Border? AccentDec1Border;
+        private Border? AccentDec2Border;
+        private Border? AccentInc1Border;
+        private Border? AccentInc2Border;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ColorPreviewer"/> class.
+        /// </summary>
+        public ColorPreviewer() : base()
+        {
+        }
+
+        /// <summary>
+        /// Connects or disconnects all control event handlers.
+        /// </summary>
+        /// <param name="connected">True to connect event handlers, otherwise false.</param>
+        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;
+            }
+        }
+
+        /// <inheritdoc/>
+        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<Border>(nameof(AccentDec1Border));
+            AccentDec2Border = e.NameScope.Find<Border>(nameof(AccentDec2Border));
+            AccentInc1Border = e.NameScope.Find<Border>(nameof(AccentInc1Border));
+            AccentInc2Border = e.NameScope.Find<Border>(nameof(AccentInc2Border));
+
+            // Must connect after controls are found
+            ConnectEvents(true);
+
+            base.OnApplyTemplate(e);
+        }
+
+        /// <inheritdoc/>
+        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+        {
+            if (change.Property == HsvColorProperty)
+            {
+                OnColorChanged(new ColorChangedEventArgs(
+                    change.GetOldValue<HsvColor>().ToRgb(),
+                    change.GetNewValue<HsvColor>().ToRgb()));
+            }
+
+            base.OnPropertyChanged(change);
+        }
+
+        /// <summary>
+        /// Called before the <see cref="ColorChanged"/> event occurs.
+        /// </summary>
+        /// <param name="e">The <see cref="ColorChangedEventArgs"/> defining old/new colors.</param>
+        protected virtual void OnColorChanged(ColorChangedEventArgs e)
+        {
+            ColorChanged?.Invoke(this, e);
+        }
+
+        /// <summary>
+        /// Event handler for when an accent color border is pressed.
+        /// This will update the color to the background of the pressed panel.
+        /// </summary>
+        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()));
+        }
+    }
+}

+ 146 - 0
src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs

@@ -0,0 +1,146 @@
+using Avalonia.Data;
+using Avalonia.Media;
+
+namespace Avalonia.Controls.Primitives
+{
+    /// <inheritdoc/>
+    public partial class ColorSlider
+    {
+        /// <summary>
+        /// Defines the <see cref="Color"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Color> ColorProperty =
+            AvaloniaProperty.Register<ColorSlider, Color>(
+                nameof(Color),
+                Colors.White,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="ColorComponent"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ColorComponent> ColorComponentProperty =
+            AvaloniaProperty.Register<ColorSlider, ColorComponent>(
+                nameof(ColorComponent),
+                ColorComponent.Component1);
+
+        /// <summary>
+        /// Defines the <see cref="ColorModel"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ColorModel> ColorModelProperty =
+            AvaloniaProperty.Register<ColorSlider, ColorModel>(
+                nameof(ColorModel),
+                ColorModel.Rgba);
+
+        /// <summary>
+        /// Defines the <see cref="HsvColor"/> property.
+        /// </summary>
+        public static readonly StyledProperty<HsvColor> HsvColorProperty =
+            AvaloniaProperty.Register<ColorSlider, HsvColor>(
+                nameof(HsvColor),
+                Colors.White.ToHsv(),
+                defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="IsAlphaMaxForced"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsAlphaMaxForcedProperty =
+            AvaloniaProperty.Register<ColorSlider, bool>(
+                nameof(IsAlphaMaxForced),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsAutoUpdatingEnabled"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsAutoUpdatingEnabledProperty =
+            AvaloniaProperty.Register<ColorSlider, bool>(
+                nameof(IsAutoUpdatingEnabled),
+                true);
+
+        /// <summary>
+        /// Defines the <see cref="IsSaturationValueMaxForced"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsSaturationValueMaxForcedProperty =
+            AvaloniaProperty.Register<ColorSlider, bool>(
+                nameof(IsSaturationValueMaxForced),
+                true);
+
+        /// <summary>
+        /// Gets or sets the currently selected color in the RGB color model.
+        /// </summary>
+        /// <remarks>
+        /// Use this property instead of <see cref="HsvColor"/> when in <see cref="ColorModel.Rgba"/>
+        /// to avoid loss of precision and color drifting.
+        /// </remarks>
+        public Color Color
+        {
+            get => GetValue(ColorProperty);
+            set => SetValue(ColorProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the color component represented by the slider.
+        /// </summary>
+        public ColorComponent ColorComponent
+        {
+            get => GetValue(ColorComponentProperty);
+            set => SetValue(ColorComponentProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the active color model used by the slider.
+        /// </summary>
+        public ColorModel ColorModel
+        {
+            get => GetValue(ColorModelProperty);
+            set => SetValue(ColorModelProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the currently selected color in the HSV color model.
+        /// </summary>
+        /// <remarks>
+        /// Use this property instead of <see cref="Color"/> when in <see cref="ColorModel.Hsva"/>
+        /// to avoid loss of precision and color drifting.
+        /// </remarks>
+        public HsvColor HsvColor
+        {
+            get => GetValue(HsvColorProperty);
+            set => SetValue(HsvColorProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether the alpha component is always forced to maximum for components
+        /// other than <see cref="ColorComponent"/>.
+        /// This ensures that the background is always visible and never transparent regardless of the actual color.
+        /// </summary>
+        public bool IsAlphaMaxForced
+        {
+            get => GetValue(IsAlphaMaxForcedProperty);
+            set => SetValue(IsAlphaMaxForcedProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether automatic background and foreground updates will be
+        /// calculated when the set color changes.
+        /// </summary>
+        /// <remarks>
+        /// This can be disabled for performance reasons when working with multiple sliders.
+        /// </remarks>
+        public bool IsAutoUpdatingEnabled
+        {
+            get => GetValue(IsAutoUpdatingEnabledProperty);
+            set => SetValue(IsAutoUpdatingEnabledProperty, value);
+        }
+
+        /// <summary>
+        /// 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 <see cref="ColorComponent"/> will be changed.
+        /// This ensures, for example, that the Hue background is always visible and never washed out regardless of the actual color.
+        /// </summary>
+        public bool IsSaturationValueMaxForced
+        {
+            get => GetValue(IsSaturationValueMaxForcedProperty);
+            set => SetValue(IsSaturationValueMaxForcedProperty, value);
+        }
+    }
+}

+ 399 - 0
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
+{
+    /// <summary>
+    /// A slider with a background that represents a single color component.
+    /// </summary>
+    [PseudoClasses(pcDarkSelector, pcLightSelector)]
+    public partial class ColorSlider : Slider
+    {
+        protected const string pcDarkSelector = ":dark-selector";
+        protected const string pcLightSelector = ":light-selector";
+
+        /// <summary>
+        /// Event for when the selected color changes within the slider.
+        /// </summary>
+        public event EventHandler<ColorChangedEventArgs>? ColorChanged;
+
+        private const double MaxHue = 359.99999999999999999; // 17 decimal places
+        private bool disableUpdates = false;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ColorSlider"/> class.
+        /// </summary>
+        public ColorSlider() : base()
+        {
+        }
+
+        /// <summary>
+        /// Updates the visual state of the control by applying latest PseudoClasses.
+        /// </summary>
+        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);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Generates a new background image for the color slider and applies it.
+        /// </summary>
+        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));
+                }
+            }
+        }
+
+        /// <summary>
+        /// Updates the slider property values by applying the current color.
+        /// </summary>
+        /// <remarks>
+        /// Warning: This will trigger property changed updates.
+        /// Consider using <see cref="disableUpdates"/> externally.
+        /// </remarks>
+        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;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Gets the current color determined by the slider values.
+        /// </summary>
+        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());
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <param name="hsvColor">The actual color to get the equivalent background color for.</param>
+        /// <returns>The equivalent, perceived background color.</returns>
+        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;
+            }
+        }
+
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <param name="rgbColor">The actual color to get the equivalent background color for.</param>
+        /// <returns>The equivalent, perceived background color.</returns>
+        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;
+        }
+
+        /// <inheritdoc/>
+        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<Color>(),
+                    change.GetNewValue<Color>()));
+
+                disableUpdates = false;
+            }
+            else if (change.Property == HsvColorProperty)
+            {
+                disableUpdates = true;
+
+                Color = HsvColor.ToRgb();
+
+                if (IsAutoUpdatingEnabled)
+                {
+                    SetColorToSliderValues();
+                    UpdateBackground();
+                }
+
+                UpdatePseudoClasses();
+                OnColorChanged(new ColorChangedEventArgs(
+                    change.GetOldValue<HsvColor>().ToRgb(),
+                    change.GetNewValue<HsvColor>().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);
+        }
+
+        /// <summary>
+        /// Called before the <see cref="ColorChanged"/> event occurs.
+        /// </summary>
+        /// <param name="e">The <see cref="ColorChangedEventArgs"/> defining old/new colors.</param>
+        protected virtual void OnColorChanged(ColorChangedEventArgs e)
+        {
+            ColorChanged?.Invoke(this, e);
+        }
+    }
+}

+ 0 - 414
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs

@@ -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<byte> 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;
-        }
-
-        /// <summary>
-        /// Gets the relative (perceptual) luminance/brightness of the given color.
-        /// 1 is closer to white while 0 is closer to black.
-        /// </summary>
-        /// <param name="color">The color to calculate relative luminance for.</param>
-        /// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
-        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);
-        }
-    }
-}

+ 83 - 68
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@@ -3,6 +3,7 @@
 //
 //
 // Licensed to The Avalonia Project under the MIT License.
 // Licensed to The Avalonia Project under the MIT License.
 
 
+using Avalonia.Data;
 using Avalonia.Media;
 using Avalonia.Media;
 
 
 namespace Avalonia.Controls.Primitives
 namespace Avalonia.Controls.Primitives
@@ -10,6 +11,88 @@ namespace Avalonia.Controls.Primitives
     /// <inheritdoc/>
     /// <inheritdoc/>
     public partial class ColorSpectrum
     public partial class ColorSpectrum
     {
     {
+        /// <summary>
+        /// Defines the <see cref="Color"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Color> ColorProperty =
+            AvaloniaProperty.Register<ColorSpectrum, Color>(
+                nameof(Color),
+                Colors.White,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="Components"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ColorSpectrumComponents> ComponentsProperty =
+            AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumComponents>(
+                nameof(Components),
+                ColorSpectrumComponents.HueSaturation);
+
+        /// <summary>
+        /// Defines the <see cref="HsvColor"/> property.
+        /// </summary>
+        public static readonly StyledProperty<HsvColor> HsvColorProperty =
+            AvaloniaProperty.Register<ColorSpectrum, HsvColor>(
+                nameof(HsvColor),
+                Colors.White.ToHsv(),
+                defaultBindingMode: BindingMode.TwoWay);
+
+        /// <summary>
+        /// Defines the <see cref="MaxHue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaxHueProperty =
+            AvaloniaProperty.Register<ColorSpectrum, int>(
+                nameof(MaxHue),
+                359);
+
+        /// <summary>
+        /// Defines the <see cref="MaxSaturation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaxSaturationProperty =
+            AvaloniaProperty.Register<ColorSpectrum, int>(
+                nameof(MaxSaturation),
+                100);
+
+        /// <summary>
+        /// Defines the <see cref="MaxValue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MaxValueProperty =
+            AvaloniaProperty.Register<ColorSpectrum, int>(
+                nameof(MaxValue),
+                100);
+
+        /// <summary>
+        /// Defines the <see cref="MinHue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MinHueProperty =
+            AvaloniaProperty.Register<ColorSpectrum, int>(
+                nameof(MinHue),
+                0);
+
+        /// <summary>
+        /// Defines the <see cref="MinSaturation"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MinSaturationProperty =
+            AvaloniaProperty.Register<ColorSpectrum, int>(
+                nameof(MinSaturation),
+                0);
+
+        /// <summary>
+        /// Defines the <see cref="MinValue"/> property.
+        /// </summary>
+        public static readonly StyledProperty<int> MinValueProperty =
+            AvaloniaProperty.Register<ColorSpectrum, int>(
+                nameof(MinValue),
+                0);
+
+        /// <summary>
+        /// Defines the <see cref="Shape"/> property.
+        /// </summary>
+        public static readonly StyledProperty<ColorSpectrumShape> ShapeProperty =
+            AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumShape>(
+                nameof(Shape),
+                ColorSpectrumShape.Box);
+
         /// <summary>
         /// <summary>
         /// Gets or sets the currently selected color in the RGB color model.
         /// Gets or sets the currently selected color in the RGB color model.
         /// </summary>
         /// </summary>
@@ -23,14 +106,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(ColorProperty, value);
             set => SetValue(ColorProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="Color"/> property.
-        /// </summary>
-        public static readonly StyledProperty<Color> ColorProperty =
-            AvaloniaProperty.Register<ColorSpectrum, Color>(
-                nameof(Color),
-                Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF));
-
         /// <summary>
         /// <summary>
         /// Gets or sets the two HSV color components displayed by the spectrum.
         /// Gets or sets the two HSV color components displayed by the spectrum.
         /// </summary>
         /// </summary>
@@ -43,14 +118,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(ComponentsProperty, value);
             set => SetValue(ComponentsProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="Components"/> property.
-        /// </summary>
-        public static readonly StyledProperty<ColorSpectrumComponents> ComponentsProperty =
-            AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumComponents>(
-                nameof(Components),
-                ColorSpectrumComponents.HueSaturation);
-
         /// <summary>
         /// <summary>
         /// Gets or sets the currently selected color in the HSV color model.
         /// Gets or sets the currently selected color in the HSV color model.
         /// </summary>
         /// </summary>
@@ -65,14 +132,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(HsvColorProperty, value);
             set => SetValue(HsvColorProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="HsvColor"/> property.
-        /// </summary>
-        public static readonly StyledProperty<HsvColor> HsvColorProperty =
-            AvaloniaProperty.Register<ColorSpectrum, HsvColor>(
-                nameof(HsvColor),
-                new HsvColor(1, 0, 0, 1));
-
         /// <summary>
         /// <summary>
         /// Gets or sets the maximum value of the Hue component in the range from 0..359.
         /// Gets or sets the maximum value of the Hue component in the range from 0..359.
         /// This property must be greater than <see cref="MinHue"/>.
         /// This property must be greater than <see cref="MinHue"/>.
@@ -86,12 +145,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(MaxHueProperty, value);
             set => SetValue(MaxHueProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="MaxHue"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> MaxHueProperty =
-            AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxHue), 359);
-
         /// <summary>
         /// <summary>
         /// Gets or sets the maximum value of the Saturation component in the range from 0..100.
         /// Gets or sets the maximum value of the Saturation component in the range from 0..100.
         /// This property must be greater than <see cref="MinSaturation"/>.
         /// This property must be greater than <see cref="MinSaturation"/>.
@@ -105,12 +158,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(MaxSaturationProperty, value);
             set => SetValue(MaxSaturationProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="MaxSaturation"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> MaxSaturationProperty =
-            AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxSaturation), 100);
-
         /// <summary>
         /// <summary>
         /// Gets or sets the maximum value of the Value component in the range from 0..100.
         /// Gets or sets the maximum value of the Value component in the range from 0..100.
         /// This property must be greater than <see cref="MinValue"/>.
         /// This property must be greater than <see cref="MinValue"/>.
@@ -124,12 +171,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(MaxValueProperty, value);
             set => SetValue(MaxValueProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="MaxValue"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> MaxValueProperty =
-            AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MaxValue), 100);
-
         /// <summary>
         /// <summary>
         /// Gets or sets the minimum value of the Hue component in the range from 0..359.
         /// Gets or sets the minimum value of the Hue component in the range from 0..359.
         /// This property must be less than <see cref="MaxHue"/>.
         /// This property must be less than <see cref="MaxHue"/>.
@@ -143,12 +184,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(MinHueProperty, value);
             set => SetValue(MinHueProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="MinHue"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> MinHueProperty =
-            AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinHue), 0);
-
         /// <summary>
         /// <summary>
         /// Gets or sets the minimum value of the Saturation component in the range from 0..100.
         /// Gets or sets the minimum value of the Saturation component in the range from 0..100.
         /// This property must be less than <see cref="MaxSaturation"/>.
         /// This property must be less than <see cref="MaxSaturation"/>.
@@ -162,12 +197,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(MinSaturationProperty, value);
             set => SetValue(MinSaturationProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="MinSaturation"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> MinSaturationProperty =
-            AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinSaturation), 0);
-
         /// <summary>
         /// <summary>
         /// Gets or sets the minimum value of the Value component in the range from 0..100.
         /// Gets or sets the minimum value of the Value component in the range from 0..100.
         /// This property must be less than <see cref="MaxValue"/>.
         /// This property must be less than <see cref="MaxValue"/>.
@@ -181,12 +210,6 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(MinValueProperty, value);
             set => SetValue(MinValueProperty, value);
         }
         }
 
 
-        /// <summary>
-        /// Defines the <see cref="MinValue"/> property.
-        /// </summary>
-        public static readonly StyledProperty<int> MinValueProperty =
-            AvaloniaProperty.Register<ColorSpectrum, int>(nameof(MinValue), 0);
-
         /// <summary>
         /// <summary>
         /// Gets or sets the displayed shape of the spectrum.
         /// Gets or sets the displayed shape of the spectrum.
         /// </summary>
         /// </summary>
@@ -195,13 +218,5 @@ namespace Avalonia.Controls.Primitives
             get => GetValue(ShapeProperty);
             get => GetValue(ShapeProperty);
             set => SetValue(ShapeProperty, value);
             set => SetValue(ShapeProperty, value);
         }
         }
-
-        /// <summary>
-        /// Defines the <see cref="Shape"/> property.
-        /// </summary>
-        public static readonly StyledProperty<ColorSpectrumShape> ShapeProperty =
-            AvaloniaProperty.Register<ColorSpectrum, ColorSpectrumShape>(
-                nameof(Shape),
-                ColorSpectrumShape.Box);
     }
     }
 }
 }

+ 83 - 55
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@@ -10,6 +10,7 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Shapes;
 using Avalonia.Input;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
+using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using Avalonia.Media.Imaging;
 using Avalonia.Threading;
 using Avalonia.Threading;
@@ -20,7 +21,6 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// <summary>
     /// A two dimensional spectrum for color selection.
     /// A two dimensional spectrum for color selection.
     /// </summary>
     /// </summary>
-    [TemplatePart("PART_ColorNameToolTip",         typeof(ToolTip))]
     [TemplatePart("PART_InputTarget",              typeof(Canvas))]
     [TemplatePart("PART_InputTarget",              typeof(Canvas))]
     [TemplatePart("PART_LayoutRoot",               typeof(Panel))]
     [TemplatePart("PART_LayoutRoot",               typeof(Panel))]
     [TemplatePart("PART_SelectionEllipsePanel",    typeof(Panel))]
     [TemplatePart("PART_SelectionEllipsePanel",    typeof(Panel))]
@@ -29,10 +29,11 @@ namespace Avalonia.Controls.Primitives
     [TemplatePart("PART_SpectrumRectangle",        typeof(Rectangle))]
     [TemplatePart("PART_SpectrumRectangle",        typeof(Rectangle))]
     [TemplatePart("PART_SpectrumOverlayEllipse",   typeof(Ellipse))]
     [TemplatePart("PART_SpectrumOverlayEllipse",   typeof(Ellipse))]
     [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))]
     [TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))]
-    [PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)]
+    [PseudoClasses(pcPressed, pcLargeSelector, pcDarkSelector, pcLightSelector)]
     public partial class ColorSpectrum : TemplatedControl
     public partial class ColorSpectrum : TemplatedControl
     {
     {
         protected const string pcPressed       = ":pressed";
         protected const string pcPressed       = ":pressed";
+        protected const string pcDarkSelector  = ":dark-selector";
         protected const string pcLargeSelector = ":large-selector";
         protected const string pcLargeSelector = ":large-selector";
         protected const string pcLightSelector = ":light-selector";
         protected const string pcLightSelector = ":light-selector";
 
 
@@ -60,7 +61,6 @@ namespace Avalonia.Controls.Primitives
         private Ellipse? _spectrumOverlayEllipse;
         private Ellipse? _spectrumOverlayEllipse;
         private Canvas? _inputTarget;
         private Canvas? _inputTarget;
         private Panel? _selectionEllipsePanel;
         private Panel? _selectionEllipsePanel;
-        private ToolTip? _colorNameToolTip;
 
 
         // Put the spectrum images in a bitmap, which is then given to an ImageBrush.
         // Put the spectrum images in a bitmap, which is then given to an ImageBrush.
         private WriteableBitmap? _hueRedBitmap;
         private WriteableBitmap? _hueRedBitmap;
@@ -117,7 +117,6 @@ namespace Avalonia.Controls.Primitives
 
 
             UnregisterEvents(); // Failsafe
             UnregisterEvents(); // Failsafe
 
 
-            _colorNameToolTip = e.NameScope.Find<ToolTip>("PART_ColorNameToolTip");
             _inputTarget = e.NameScope.Find<Canvas>("PART_InputTarget");
             _inputTarget = e.NameScope.Find<Canvas>("PART_InputTarget");
             _layoutRoot = e.NameScope.Find<Panel>("PART_LayoutRoot");
             _layoutRoot = e.NameScope.Find<Panel>("PART_LayoutRoot");
             _selectionEllipsePanel = e.NameScope.Find<Panel>("PART_SelectionEllipsePanel");
             _selectionEllipsePanel = e.NameScope.Find<Panel>("PART_SelectionEllipsePanel");
@@ -152,10 +151,10 @@ namespace Avalonia.Controls.Primitives
                 });
                 });
             }
             }
 
 
-            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.
             // If we haven't yet created our bitmaps, do so now.
@@ -320,7 +319,7 @@ namespace Avalonia.Controls.Primitives
             IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small;
             IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small;
 
 
             HsvColor hsvColor = HsvColor;
             HsvColor hsvColor = HsvColor;
-            UpdateColor(ColorHelpers.IncrementColorComponent(
+            UpdateColor(ColorPickerHelpers.IncrementColorComponent(
                 new Hsv(hsvColor),
                 new Hsv(hsvColor),
                 incrementComponent,
                 incrementComponent,
                 direction,
                 direction,
@@ -330,34 +329,51 @@ namespace Avalonia.Controls.Primitives
                 maxBound));
                 maxBound));
 
 
             e.Handled = true;
             e.Handled = true;
-
-            return;
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnGotFocus(GotFocusEventArgs e)
         protected override void OnGotFocus(GotFocusEventArgs e)
         {
         {
             // We only want to bother with the color name tool tip if we can provide color names.
             // 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();
             UpdatePseudoClasses();
+
+            base.OnGotFocus(e);
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         protected override void OnLostFocus(RoutedEventArgs e)
         protected override void OnLostFocus(RoutedEventArgs e)
         {
         {
             // We only want to bother with the color name tool tip if we can provide color names.
             // 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();
             UpdatePseudoClasses();
+
+            base.OnLostFocus(e);
+        }
+
+        /// <inheritdoc/>
+        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);
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
@@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives
                 var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor);
                 var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor);
                 ColorChanged?.Invoke(this, colorChangedEventArgs);
                 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 @@ namespace Avalonia.Controls.Primitives
                 PseudoClasses.Set(pcLargeSelector, false);
                 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)
         private void UpdateColor(Hsv newHsv)
@@ -575,8 +598,10 @@ namespace Avalonia.Controls.Primitives
                 return;
                 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 radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2;
             double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2));
             double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2));
 
 
@@ -807,19 +832,17 @@ namespace Avalonia.Controls.Primitives
                 yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius;
                 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.
             // 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();
             UpdatePseudoClasses();
@@ -961,7 +984,14 @@ namespace Avalonia.Controls.Primitives
             List<byte> bgraMaxPixelData = new List<byte>();
             List<byte> bgraMaxPixelData = new List<byte>();
             List<Hsv> newHsvValues = new List<Hsv>();
             List<Hsv> newHsvValues = new List<Hsv>();
 
 
-            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;
             var pixelDataSize = pixelCount * 4;
             bgraMinPixelData.Capacity = pixelDataSize;
             bgraMinPixelData.Capacity = pixelDataSize;
 
 
@@ -978,8 +1008,6 @@ namespace Avalonia.Controls.Primitives
             bgraMaxPixelData.Capacity = pixelDataSize;
             bgraMaxPixelData.Capacity = pixelDataSize;
             newHsvValues.Capacity = pixelCount;
             newHsvValues.Capacity = pixelCount;
 
 
-            int minDimensionInt = (int)Math.Round(minDimension);
-
             await Task.Run(() =>
             await Task.Run(() =>
             {
             {
                 // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes,
                 // As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes,
@@ -998,12 +1026,12 @@ namespace Avalonia.Controls.Primitives
                 // but the running time savings after that are *huge* when we can just set an opacity instead of generating a brand new bitmap.
                 // 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)
                 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(
                             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,
                                 bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData,
                                 newHsvValues);
                                 newHsvValues);
                         }
                         }
@@ -1011,12 +1039,12 @@ namespace Avalonia.Controls.Primitives
                 }
                 }
                 else
                 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(
                             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,
                                 bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData,
                                 newHsvValues);
                                 newHsvValues);
                         }
                         }
@@ -1026,13 +1054,13 @@ namespace Avalonia.Controls.Primitives
 
 
             Dispatcher.UIThread.Post(() =>
             Dispatcher.UIThread.Post(() =>
             {
             {
-                int pixelWidth = (int)Math.Round(minDimension);
-                int pixelHeight = (int)Math.Round(minDimension);
+                int pixelWidth = pixelDimension;
+                int pixelHeight = pixelDimension;
 
 
                 ColorSpectrumComponents components2 = Components;
                 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)
                 switch (components2)
                 {
                 {
@@ -1048,18 +1076,18 @@ namespace Avalonia.Controls.Primitives
                     case ColorSpectrumComponents.ValueSaturation:
                     case ColorSpectrumComponents.ValueSaturation:
                     case ColorSpectrumComponents.SaturationValue:
                     case ColorSpectrumComponents.SaturationValue:
                         _hueRedBitmap = minBitmap;
                         _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;
                         _huePurpleBitmap = maxBitmap;
                         break;
                         break;
                 }
                 }
 
 
                 _shapeFromLastBitmapCreation = Shape;
                 _shapeFromLastBitmapCreation = Shape;
                 _componentsFromLastBitmapCreation = Components;
                 _componentsFromLastBitmapCreation = Components;
-                _imageWidthFromLastBitmapCreation = minDimension;
-                _imageHeightFromLastBitmapCreation = minDimension;
+                _imageWidthFromLastBitmapCreation = pixelDimension;
+                _imageHeightFromLastBitmapCreation = pixelDimension;
                 _minHueFromLastBitmapCreation = MinHue;
                 _minHueFromLastBitmapCreation = MinHue;
                 _maxHueFromLastBitmapCreation = MaxHue;
                 _maxHueFromLastBitmapCreation = MaxHue;
                 _minSaturationFromLastBitmapCreation = MinSaturation;
                 _minSaturationFromLastBitmapCreation = MinSaturation;
@@ -1078,7 +1106,7 @@ namespace Avalonia.Controls.Primitives
             double x,
             double x,
             double y,
             double y,
             Hsv baseHsv,
             Hsv baseHsv,
-            double minDimension,
+            int minDimension,
             ColorSpectrumComponents components,
             ColorSpectrumComponents components,
             double minHue,
             double minHue,
             double maxHue,
             double maxHue,
@@ -1570,7 +1598,7 @@ namespace Avalonia.Controls.Primitives
                 displayedColor = Color;
                 displayedColor = Color;
             }
             }
 
 
-            var lum = ColorHelpers.GetRelativeLuminance(displayedColor);
+            var lum = ColorHelper.GetRelativeLuminance(displayedColor);
 
 
             return lum <= 0.5;
             return lum <= 0.5;
         }
         }

+ 0 - 0
src/Avalonia.Controls.ColorPicker/ColorSpectrumComponents.cs → src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs


+ 0 - 0
src/Avalonia.Controls.ColorPicker/ColorSpectrumShape.cs → src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs


+ 116 - 0
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
+{
+    /// <summary>
+    /// Creates an accent color for a given base color value and step parameter.
+    /// This is a highly-specialized converter for the color picker.
+    /// </summary>
+    public class AccentColorConverter : IValueConverter
+    {
+        /// <summary>
+        /// The amount to change the Value component for each accent color step.
+        /// </summary>
+        public const double ValueDelta = 0.1;
+
+        /// <inheritdoc/>
+        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;
+            }
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return AvaloniaProperty.UnsetValue;
+        }
+
+        /// <summary>
+        /// This does not account for perceptual differences and also does not match with
+        /// system accent color calculation.
+        /// </summary>
+        /// <remarks>
+        /// 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.
+        /// </remarks>
+        /// <param name="hsvColor">The base color to calculate the accent from.</param>
+        /// <param name="accentStep">The number of accent color steps to move.</param>
+        /// <returns>The new accent color.</returns>
+        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;
+            }
+        }
+    }
+}

+ 68 - 0
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
+{
+    /// <summary>
+    /// Gets the approximated display name for the color.
+    /// </summary>
+    public class ColorToDisplayNameConverter : IValueConverter
+    {
+        /// <inheritdoc/>
+        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);
+            }
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return AvaloniaProperty.UnsetValue;
+        }
+    }
+}

+ 82 - 0
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
+{
+    /// <summary>
+    /// Converts a color to a hex string and vice versa.
+    /// </summary>
+    public class ColorToHexConverter : IValueConverter
+    {
+        /// <inheritdoc/>
+        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;
+        }
+
+        /// <inheritdoc/>
+        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;
+            }
+        }
+    }
+}

+ 51 - 0
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
+{
+    /// <summary>
+    /// Gets the third <see cref="ColorComponent"/> corresponding with a given
+    /// <see cref="ColorSpectrumComponents"/> that represents the other two components.
+    /// This is a highly-specialized converter for the color picker.
+    /// </summary>
+    public class ThirdComponentConverter : IValueConverter
+    {
+        /// <inheritdoc/>
+        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;
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return AvaloniaProperty.UnsetValue;
+        }
+    }
+}

+ 50 - 0
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
+{
+    /// <summary>
+    /// Converts the given value into an <see cref="IBrush"/> when a conversion is possible.
+    /// </summary>
+    public class ToBrushConverter : IValueConverter
+    {
+        /// <inheritdoc/>
+        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;
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return AvaloniaProperty.UnsetValue;
+        }
+    }
+}

+ 58 - 0
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
+{
+    /// <summary>
+    /// Converts the given value into a <see cref="Color"/> when a conversion is possible.
+    /// </summary>
+    public class ToColorConverter : IValueConverter
+    {
+        /// <inheritdoc/>
+        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;
+        }
+
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
+        {
+            return AvaloniaProperty.UnsetValue;
+        }
+    }
+}

+ 50 - 0
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
+{
+    /// <summary>
+    /// Converter to chain together multiple converters.
+    /// </summary>
+    public class ValueConverterGroup : List<IValueConverter>, IValueConverter
+    {
+        /// <inheritdoc/>
+        /// <inheritdoc/>
+        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;
+        }
+
+        /// <inheritdoc/>
+        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;
+        }
+    }
+}

+ 142 - 0
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
+{
+    /// <summary>
+    /// Contains helpers useful when working with colors.
+    /// </summary>
+    public static class ColorHelper
+    {
+        private static readonly Dictionary<Color, string> cachedDisplayNames = new Dictionary<Color, string>();
+        private static readonly object cacheMutex = new object();
+
+        /// <summary>
+        /// Gets the relative (perceptual) luminance/brightness of the given color.
+        /// 1 is closer to white while 0 is closer to black.
+        /// </summary>
+        /// <param name="color">The color to calculate relative luminance for.</param>
+        /// <returns>The relative (perceptual) luminance/brightness of the given color.</returns>
+        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);
+        }
+
+        /// <summary>
+        /// Determines if color display names are supported based on the current thread culture.
+        /// </summary>
+        /// <remarks>
+        /// Only English names are currently supported following known color names.
+        /// In the future known color names could be localized.
+        /// </remarks>
+        public static bool ToDisplayNameExists
+        {
+            get => CultureInfo.CurrentUICulture.Name.StartsWith("EN", StringComparison.OrdinalIgnoreCase);
+        }
+
+        /// <summary>
+        /// Determines an approximate display name for the given color.
+        /// </summary>
+        /// <param name="color">The color to get the display name for.</param>
+        /// <returns>The approximate color display name.</returns>
+        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;
+            }
+        }
+    }
+}

+ 629 - 0
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
+{
+    /// <summary>
+    /// Contains internal, special-purpose helpers used with the color picker.
+    /// </summary>
+    internal static class ColorPickerHelpers
+    {
+        /// <summary>
+        /// 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.
+        /// </summary>
+        /// <param name="width">The pixel width (X, horizontal) of the resulting bitmap.</param>
+        /// <param name="height">The pixel height (Y, vertical) of the resulting bitmap.</param>
+        /// <param name="orientation">The orientation of the resulting bitmap (gradient direction).</param>
+        /// <param name="colorModel">The color model being used: RGBA or HSVA.</param>
+        /// <param name="component">The specific color component to sweep.</param>
+        /// <param name="baseHsvColor">The base HSV color used for components not being changed.</param>
+        /// <param name="isAlphaMaxForced">Fix the alpha component value to maximum during calculation.
+        /// This will remove any alpha/transparency from the other component backgrounds.</param>
+        /// <param name="isSaturationValueMaxForced">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.</param>
+        /// <returns>A new bitmap representing a gradient of color component values.</returns>
+        public static async Task<byte[]> 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<byte[]>(() =>
+            {
+                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;
+        }
+
+        /// <summary>
+        /// Converts the given raw BGRA pre-multiplied alpha pixel data into a bitmap.
+        /// </summary>
+        /// <param name="bgraPixelData">The bitmap (in raw BGRA pre-multiplied alpha pixels).</param>
+        /// <param name="pixelWidth">The pixel width of the bitmap.</param>
+        /// <param name="pixelHeight">The pixel height of the bitmap.</param>
+        /// <returns>A new <see cref="WriteableBitmap"/>.</returns>
+        public static WriteableBitmap CreateBitmapFromPixelData(
+            IList<byte> 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;
+        }
+    }
+}

+ 0 - 0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs → src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs


+ 0 - 0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementAmount.cs → src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs


+ 0 - 0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/IncrementDirection.cs → src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs


+ 0 - 0
src/Avalonia.Controls.ColorPicker/ColorSpectrum/Rgb.cs → src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs


+ 11 - 11
src/Avalonia.Controls.ColorPicker/HsvComponent.cs

@@ -12,13 +12,21 @@ namespace Avalonia.Controls
     /// </summary>
     /// </summary>
     public enum HsvComponent
     public enum HsvComponent
     {
     {
+        /// <summary>
+        /// The Alpha component.
+        /// </summary>
+        /// <remarks>
+        /// Also see: <see cref="HsvColor.A"/>
+        /// </remarks>
+        Alpha = 0,
+
         /// <summary>
         /// <summary>
         /// The Hue component.
         /// The Hue component.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// Also see: <see cref="HsvColor.H"/>
         /// Also see: <see cref="HsvColor.H"/>
         /// </remarks>
         /// </remarks>
-        Hue,
+        Hue = 1,
 
 
         /// <summary>
         /// <summary>
         /// The Saturation component.
         /// The Saturation component.
@@ -26,7 +34,7 @@ namespace Avalonia.Controls
         /// <remarks>
         /// <remarks>
         /// Also see: <see cref="HsvColor.S"/>
         /// Also see: <see cref="HsvColor.S"/>
         /// </remarks>
         /// </remarks>
-        Saturation,
+        Saturation = 2,
 
 
         /// <summary>
         /// <summary>
         /// The Value component.
         /// The Value component.
@@ -34,14 +42,6 @@ namespace Avalonia.Controls
         /// <remarks>
         /// <remarks>
         /// Also see: <see cref="HsvColor.V"/>
         /// Also see: <see cref="HsvColor.V"/>
         /// </remarks>
         /// </remarks>
-        Value,
-
-        /// <summary>
-        /// The Alpha component.
-        /// </summary>
-        /// <remarks>
-        /// Also see: <see cref="HsvColor.A"/>
-        /// </remarks>
-        Alpha
+        Value = 3
     };
     };
 }
 }

+ 42 - 0
src/Avalonia.Controls.ColorPicker/RgbComponent.cs

@@ -0,0 +1,42 @@
+using Avalonia.Media;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Defines a specific component in the RGB color model.
+    /// </summary>
+    public enum RgbComponent
+    {
+        /// <summary>
+        /// The Alpha component.
+        /// </summary>
+        /// <remarks>
+        /// Also see: <see cref="Color.A"/>
+        /// </remarks>
+        Alpha = 0,
+
+        /// <summary>
+        /// The Red component.
+        /// </summary>
+        /// <remarks>
+        /// Also see: <see cref="Color.R"/>
+        /// </remarks>
+        Red = 1,
+
+        /// <summary>
+        /// The Green component.
+        /// </summary>
+        /// <remarks>
+        /// Also see: <see cref="Color.G"/>
+        /// </remarks>
+        Green = 2,
+
+        /// <summary>
+        /// The Blue component.
+        /// </summary>
+        /// <remarks>
+        /// Also see: <see cref="Color.B"/>
+        /// </remarks>
+        Blue = 3
+    };
+}

+ 86 - 0
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml

@@ -0,0 +1,86 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:converters="using:Avalonia.Controls.Converters"
+        xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
+        x:CompileBindings="True">
+
+  <Styles.Resources>
+    <pc:AccentColorConverter x:Key="AccentColor" />
+    <converters:ToBrushConverter x:Key="ToBrush" />
+    <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
+    <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
+  </Styles.Resources>
+
+  <Style Selector="ColorPreviewer">
+    <Setter Property="Height" Value="70" />
+    <Setter Property="CornerRadius" Value="0" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Grid ColumnDefinitions="Auto,*,Auto">
+          <!-- Left accent colors -->
+          <Grid Grid.Column="0"
+                Height="40"
+                Width="80"
+                ColumnDefinitions="*,*"
+                Margin="0,0,-10,0"
+                VerticalAlignment="Center"
+                IsVisible="{TemplateBinding ShowAccentColors}">
+            <Border Grid.Column="0"
+                    Grid.ColumnSpan="2"
+                    HorizontalAlignment="Stretch"
+                    VerticalAlignment="Stretch"
+                    Background="{StaticResource CheckeredBackgroundBrush}" />
+            <Border x:Name="AccentDec2Border"
+                    Grid.Column="0"
+                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
+                    Tag="-2"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" />
+            <Border x:Name="AccentDec1Border"
+                    Grid.Column="1"
+                    Tag="-1"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" />
+          </Grid>
+          <!-- Right accent colors -->
+          <Grid Grid.Column="2"
+                Height="40"
+                Width="80"
+                ColumnDefinitions="*,*"
+                Margin="-10,0,0,0"
+                VerticalAlignment="Center"
+                IsVisible="{TemplateBinding ShowAccentColors}">
+            <Border Grid.Column="0"
+                    Grid.ColumnSpan="2"
+                    HorizontalAlignment="Stretch"
+                    VerticalAlignment="Stretch"
+                    Background="{StaticResource CheckeredBackgroundBrush}" />
+            <Border x:Name="AccentInc1Border"
+                    Grid.Column="0"
+                    Tag="1"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" />
+            <Border x:Name="AccentInc2Border"
+                    Grid.Column="1"
+                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
+                    Tag="2"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" />
+          </Grid>
+          <!-- Must be last for drop shadow Z-index -->
+          <Border Grid.Column="1"
+                  BoxShadow="0 0 10 2 #BF000000"
+                  CornerRadius="{TemplateBinding CornerRadius}"
+                  Margin="10">
+            <Panel>
+              <Border Background="{StaticResource CheckeredBackgroundBrush}"
+                      CornerRadius="{TemplateBinding CornerRadius}" />
+              <Border x:Name="PreviewBorder"
+                      CornerRadius="{TemplateBinding CornerRadius}"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch" />
+            </Panel>
+          </Border>
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+</Styles>

+ 194 - 0
src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml

@@ -0,0 +1,194 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:converters="using:Avalonia.Controls.Converters"
+        x:CompileBindings="True">
+
+  <Styles.Resources>
+    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
+    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
+  </Styles.Resources>
+
+  <Style Selector="Thumb.ColorSliderThumbStyle">
+    <Setter Property="BorderThickness" Value="0" />
+    <Setter Property="Template">
+      <Setter.Value>
+        <ControlTemplate>
+          <Border Background="{TemplateBinding Background}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  BorderThickness="{TemplateBinding BorderThickness}"
+                  CornerRadius="10" />
+        </ControlTemplate>
+      </Setter.Value>
+    </Setter>
+  </Style>
+
+  <Style Selector="ColorSlider:horizontal">
+    <Setter Property="BorderThickness" Value="0" />
+    <Setter Property="CornerRadius" Value="10" />
+    <Setter Property="Height" Value="20" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border BorderThickness="{TemplateBinding BorderThickness}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <Grid Margin="{TemplateBinding Padding}">
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{StaticResource CheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{TemplateBinding Background}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Track Name="PART_Track"
+                   HorizontalAlignment="Stretch"
+                   VerticalAlignment="Stretch"
+                   Minimum="{TemplateBinding Minimum}"
+                   Maximum="{TemplateBinding Maximum}"
+                   Value="{TemplateBinding Value, Mode=TwoWay}"
+                   IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
+                   Orientation="Horizontal">
+              <Track.DecreaseButton>
+                <RepeatButton Name="PART_DecreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.DecreaseButton>
+              <Track.IncreaseButton>
+                <RepeatButton Name="PART_IncreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.IncreaseButton>
+              <Thumb Classes="ColorSliderThumbStyle"
+                     Name="ColorSliderThumb"
+                     Margin="0"
+                     Padding="0"
+                     DataContext="{TemplateBinding Value}"
+                     Height="{TemplateBinding Height}"
+                     Width="{TemplateBinding Height}" />
+            </Track>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="ColorSlider:vertical">
+    <Setter Property="BorderThickness" Value="0" />
+    <Setter Property="CornerRadius" Value="10" />
+    <Setter Property="Width" Value="20" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border BorderThickness="{TemplateBinding BorderThickness}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <Grid Margin="{TemplateBinding Padding}">
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{StaticResource CheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{TemplateBinding Background}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Track Name="PART_Track"
+                   HorizontalAlignment="Stretch"
+                   VerticalAlignment="Stretch"
+                   Minimum="{TemplateBinding Minimum}"
+                   Maximum="{TemplateBinding Maximum}"
+                   Value="{TemplateBinding Value, Mode=TwoWay}"
+                   IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
+                   Orientation="Vertical">
+              <Track.DecreaseButton>
+                <RepeatButton Name="PART_DecreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.DecreaseButton>
+              <Track.IncreaseButton>
+                <RepeatButton Name="PART_IncreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.IncreaseButton>
+              <Thumb Classes="ColorSliderThumbStyle"
+                     Name="ColorSliderThumb"
+                     Margin="0"
+                     Padding="0"
+                     DataContext="{TemplateBinding Value}"
+                     Height="{TemplateBinding Width}"
+                     Width="{TemplateBinding Width}" />
+            </Track>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <!-- Normal State -->
+  <Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeForegroundBrush}" />
+    <Setter Property="BorderThickness" Value="3" />
+  </Style>
+
+  <!-- Selector/Thumb Color -->
+  <Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Opacity" Value="0.75" />
+  </Style>
+  <Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Opacity" Value="0.7" />
+  </Style>
+  <Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Opacity" Value="0.8" />
+  </Style>
+
+  <Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="BorderBrush" Value="Black" />
+  </Style>
+  <Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="BorderBrush" Value="White" />
+  </Style>
+
+</Styles>

+ 11 - 11
src/Avalonia.Controls.ColorPicker/Themes/Default.xaml → src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml

@@ -1,7 +1,7 @@
-<Styles xmlns="https://github.com/avaloniaui"
+<Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        x:CompileBindings="True"
-        xmlns:converters="using:Avalonia.Controls.Converters">
+        xmlns:converters="using:Avalonia.Controls.Converters"
+        x:CompileBindings="True">
 
 
   <Styles.Resources>
   <Styles.Resources>
     <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
     <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
@@ -48,7 +48,10 @@
                       Background="Transparent"
                       Background="Transparent"
                       HorizontalAlignment="Stretch"
                       HorizontalAlignment="Stretch"
                       VerticalAlignment="Stretch">
                       VerticalAlignment="Stretch">
-                <Panel x:Name="PART_SelectionEllipsePanel">
+                <!-- Note: ToolTip.VerticalOffset is for touch devices to keep the tip above fingers -->
+                <Panel x:Name="PART_SelectionEllipsePanel"
+                       ToolTip.VerticalOffset="-10"
+                       ToolTip.Placement="Top">
                   <Ellipse x:Name="FocusEllipse"
                   <Ellipse x:Name="FocusEllipse"
                            Margin="-2"
                            Margin="-2"
                            StrokeThickness="2"
                            StrokeThickness="2"
@@ -59,13 +62,10 @@
                            StrokeThickness="2"
                            StrokeThickness="2"
                            IsHitTestVisible="False"
                            IsHitTestVisible="False"
                            HorizontalAlignment="Stretch"
                            HorizontalAlignment="Stretch"
-                           VerticalAlignment="Stretch"
-                           ToolTip.VerticalOffset="-20"
-                           ToolTip.Placement="Top">
-                    <ToolTip.Tip>
-                      <ToolTip x:Name="PART_ColorNameToolTip" />
-                    </ToolTip.Tip>
-                  </Ellipse>
+                           VerticalAlignment="Stretch" />
+                  <ToolTip.Tip>
+                    <!-- Set in code-behind -->
+                  </ToolTip.Tip>
                 </Panel>
                 </Panel>
               </Canvas>
               </Canvas>
               <Rectangle x:Name="BorderRectangle"
               <Rectangle x:Name="BorderRectangle"

+ 28 - 0
src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml

@@ -0,0 +1,28 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+  <Styles.Resources>
+    <VisualBrush x:Key="CheckeredBackgroundBrush"
+                 TileMode="Tile"
+                 Stretch="Uniform"
+                 DestinationRect="0,0,8,8">
+      <VisualBrush.Visual>
+        <DrawingPresenter Width="8"
+                          Height="8">
+          <DrawingGroup>
+            <GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z"
+                             Brush="Transparent" />
+            <GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z"
+                             Brush="#19808080" />
+          </DrawingGroup>
+        </DrawingPresenter>
+      </VisualBrush.Visual>
+    </VisualBrush>
+  </Styles.Resources>
+
+  <!-- Primitives -->
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml" />
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml" />
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml" />
+
+</Styles>

+ 86 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml

@@ -0,0 +1,86 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:converters="using:Avalonia.Controls.Converters"
+        xmlns:pc="using:Avalonia.Controls.Primitives.Converters"
+        x:CompileBindings="True">
+
+  <Styles.Resources>
+    <pc:AccentColorConverter x:Key="AccentColor" />
+    <converters:ToBrushConverter x:Key="ToBrush" />
+    <converters:CornerRadiusFilterConverter x:Key="RightCornerRadiusFilterConverter" Filter="TopRight, BottomRight"/>
+    <converters:CornerRadiusFilterConverter x:Key="LeftCornerRadiusFilterConverter" Filter="TopLeft, BottomLeft"/>
+  </Styles.Resources>
+
+  <Style Selector="ColorPreviewer">
+    <Setter Property="Height" Value="70" />
+    <Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Grid ColumnDefinitions="Auto,*,Auto">
+          <!-- Left accent colors -->
+          <Grid Grid.Column="0"
+                Height="40"
+                Width="80"
+                ColumnDefinitions="*,*"
+                Margin="0,0,-10,0"
+                VerticalAlignment="Center"
+                IsVisible="{TemplateBinding ShowAccentColors}">
+            <Border Grid.Column="0"
+                    Grid.ColumnSpan="2"
+                    HorizontalAlignment="Stretch"
+                    VerticalAlignment="Stretch"
+                    Background="{StaticResource CheckeredBackgroundBrush}" />
+            <Border x:Name="AccentDec2Border"
+                    Grid.Column="0"
+                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource LeftCornerRadiusFilterConverter}}"
+                    Tag="-2"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-2'}" />
+            <Border x:Name="AccentDec1Border"
+                    Grid.Column="1"
+                    Tag="-1"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='-1'}" />
+          </Grid>
+          <!-- Right accent colors -->
+          <Grid Grid.Column="2"
+                Height="40"
+                Width="80"
+                ColumnDefinitions="*,*"
+                Margin="-10,0,0,0"
+                VerticalAlignment="Center"
+                IsVisible="{TemplateBinding ShowAccentColors}">
+            <Border Grid.Column="0"
+                    Grid.ColumnSpan="2"
+                    HorizontalAlignment="Stretch"
+                    VerticalAlignment="Stretch"
+                    Background="{StaticResource CheckeredBackgroundBrush}" />
+            <Border x:Name="AccentInc1Border"
+                    Grid.Column="0"
+                    Tag="1"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='1'}" />
+            <Border x:Name="AccentInc2Border"
+                    Grid.Column="1"
+                    CornerRadius="{TemplateBinding CornerRadius, Converter={StaticResource RightCornerRadiusFilterConverter}}"
+                    Tag="2"
+                    Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='2'}" />
+          </Grid>
+          <!-- Must be last for drop shadow Z-index -->
+          <Border Grid.Column="1"
+                  BoxShadow="0 0 10 2 #BF000000"
+                  CornerRadius="{TemplateBinding CornerRadius}"
+                  Margin="10">
+            <Panel>
+              <Border Background="{StaticResource CheckeredBackgroundBrush}"
+                      CornerRadius="{TemplateBinding CornerRadius}" />
+              <Border x:Name="PreviewBorder"
+                      CornerRadius="{TemplateBinding CornerRadius}"
+                      Background="{TemplateBinding HsvColor, Converter={StaticResource ToBrush}}"
+                      HorizontalAlignment="Stretch"
+                      VerticalAlignment="Stretch" />
+            </Panel>
+          </Border>
+        </Grid>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+</Styles>

+ 194 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml

@@ -0,0 +1,194 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:converters="using:Avalonia.Controls.Converters"
+        x:CompileBindings="True">
+
+  <Styles.Resources>
+    <converters:CornerRadiusToDoubleConverter x:Key="TopLeftCornerRadius" Corner="TopLeft" />
+    <converters:CornerRadiusToDoubleConverter x:Key="BottomRightCornerRadius" Corner="BottomRight" />
+  </Styles.Resources>
+
+  <Style Selector="Thumb.ColorSliderThumbStyle">
+    <Setter Property="BorderThickness" Value="0" />
+    <Setter Property="Template">
+      <Setter.Value>
+        <ControlTemplate>
+          <Border Background="{TemplateBinding Background}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  BorderThickness="{TemplateBinding BorderThickness}"
+                  CornerRadius="10" />
+        </ControlTemplate>
+      </Setter.Value>
+    </Setter>
+  </Style>
+
+  <Style Selector="ColorSlider:horizontal">
+    <Setter Property="BorderThickness" Value="0" />
+    <Setter Property="CornerRadius" Value="10" />
+    <Setter Property="Height" Value="20" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border BorderThickness="{TemplateBinding BorderThickness}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <Grid Margin="{TemplateBinding Padding}">
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{StaticResource CheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{TemplateBinding Background}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Track Name="PART_Track"
+                   HorizontalAlignment="Stretch"
+                   VerticalAlignment="Stretch"
+                   Minimum="{TemplateBinding Minimum}"
+                   Maximum="{TemplateBinding Maximum}"
+                   Value="{TemplateBinding Value, Mode=TwoWay}"
+                   IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
+                   Orientation="Horizontal">
+              <Track.DecreaseButton>
+                <RepeatButton Name="PART_DecreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.DecreaseButton>
+              <Track.IncreaseButton>
+                <RepeatButton Name="PART_IncreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.IncreaseButton>
+              <Thumb Classes="ColorSliderThumbStyle"
+                     Name="ColorSliderThumb"
+                     Margin="0"
+                     Padding="0"
+                     DataContext="{TemplateBinding Value}"
+                     Height="{TemplateBinding Height}"
+                     Width="{TemplateBinding Height}" />
+            </Track>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <Style Selector="ColorSlider:vertical">
+    <Setter Property="BorderThickness" Value="0" />
+    <Setter Property="CornerRadius" Value="10" />
+    <Setter Property="Width" Value="20" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border BorderThickness="{TemplateBinding BorderThickness}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <Grid Margin="{TemplateBinding Padding}">
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{StaticResource CheckeredBackgroundBrush}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Rectangle HorizontalAlignment="Stretch"
+                       VerticalAlignment="Stretch"
+                       Fill="{TemplateBinding Background}"
+                       RadiusX="{TemplateBinding CornerRadius, Converter={StaticResource TopLeftCornerRadius}}"
+                       RadiusY="{TemplateBinding CornerRadius, Converter={StaticResource BottomRightCornerRadius}}" />
+            <Track Name="PART_Track"
+                   HorizontalAlignment="Stretch"
+                   VerticalAlignment="Stretch"
+                   Minimum="{TemplateBinding Minimum}"
+                   Maximum="{TemplateBinding Maximum}"
+                   Value="{TemplateBinding Value, Mode=TwoWay}"
+                   IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
+                   Orientation="Vertical">
+              <Track.DecreaseButton>
+                <RepeatButton Name="PART_DecreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.DecreaseButton>
+              <Track.IncreaseButton>
+                <RepeatButton Name="PART_IncreaseButton"
+                              Background="Transparent"
+                              Focusable="False"
+                              HorizontalAlignment="Stretch"
+                              VerticalAlignment="Stretch">
+                  <RepeatButton.Template>
+                    <ControlTemplate>
+                      <Border Name="FocusTarget"
+                              Background="Transparent"
+                              Margin="0,-10" />
+                    </ControlTemplate>
+                  </RepeatButton.Template>
+                </RepeatButton>
+              </Track.IncreaseButton>
+              <Thumb Classes="ColorSliderThumbStyle"
+                     Name="ColorSliderThumb"
+                     Margin="0"
+                     Padding="0"
+                     DataContext="{TemplateBinding Value}"
+                     Height="{TemplateBinding Width}"
+                     Width="{TemplateBinding Width}" />
+            </Track>
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+
+  <!-- Normal State -->
+  <Style Selector="ColorSlider /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseHighBrush}" />
+    <Setter Property="BorderThickness" Value="3" />
+  </Style>
+
+  <!-- Selector/Thumb Color -->
+  <Style Selector="ColorSlider:pointerover /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Opacity" Value="0.75" />
+  </Style>
+  <Style Selector="ColorSlider:pointerover:dark-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Opacity" Value="0.7" />
+  </Style>
+  <Style Selector="ColorSlider:pointerover:light-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="Opacity" Value="0.8" />
+  </Style>
+
+  <Style Selector="ColorSlider:dark-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="BorderBrush" Value="{DynamicResource SystemControlBackgroundChromeBlackHighBrush}" />
+  </Style>
+  <Style Selector="ColorSlider:light-selector /template/ Thumb.ColorSliderThumbStyle">
+    <Setter Property="BorderBrush" Value="{DynamicResource SystemControlBackgroundChromeWhiteBrush}" />
+  </Style>
+
+</Styles>

+ 14 - 11
src/Avalonia.Controls.ColorPicker/Themes/Fluent.xaml → src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml

@@ -1,7 +1,7 @@
-<Styles xmlns="https://github.com/avaloniaui"
+<Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        x:CompileBindings="True"
-        xmlns:converters="using:Avalonia.Controls.Converters">
+        xmlns:converters="using:Avalonia.Controls.Converters"
+        x:CompileBindings="True">
 
 
   <Styles.Resources>
   <Styles.Resources>
     <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
     <converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
@@ -48,7 +48,10 @@
                       Background="Transparent"
                       Background="Transparent"
                       HorizontalAlignment="Stretch"
                       HorizontalAlignment="Stretch"
                       VerticalAlignment="Stretch">
                       VerticalAlignment="Stretch">
-                <Panel x:Name="PART_SelectionEllipsePanel">
+                <!-- Note: ToolTip.VerticalOffset is for touch devices to keep the tip above fingers -->
+                <Panel x:Name="PART_SelectionEllipsePanel"
+                       ToolTip.VerticalOffset="-10"
+                       ToolTip.Placement="Top">
                   <Ellipse x:Name="FocusEllipse"
                   <Ellipse x:Name="FocusEllipse"
                            Margin="-2"
                            Margin="-2"
                            StrokeThickness="2"
                            StrokeThickness="2"
@@ -59,13 +62,10 @@
                            StrokeThickness="2"
                            StrokeThickness="2"
                            IsHitTestVisible="False"
                            IsHitTestVisible="False"
                            HorizontalAlignment="Stretch"
                            HorizontalAlignment="Stretch"
-                           VerticalAlignment="Stretch"
-                           ToolTip.VerticalOffset="-20"
-                           ToolTip.Placement="Top">
-                    <ToolTip.Tip>
-                      <ToolTip x:Name="PART_ColorNameToolTip" />
-                    </ToolTip.Tip>
-                  </Ellipse>
+                           VerticalAlignment="Stretch" />
+                  <ToolTip.Tip>
+                    <!-- Set in code-behind -->
+                  </ToolTip.Tip>
                 </Panel>
                 </Panel>
               </Canvas>
               </Canvas>
               <Rectangle x:Name="BorderRectangle"
               <Rectangle x:Name="BorderRectangle"
@@ -118,6 +118,9 @@
   </Style>
   </Style>
 
 
   <Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse">
   <Style Selector="ColorSpectrum:pointerover /template/ Ellipse#SelectionEllipse">
+    <Setter Property="Opacity" Value="0.7" />
+  </Style>
+  <Style Selector="ColorSpectrum:pointerover:light-selector /template/ Ellipse#SelectionEllipse">
     <Setter Property="Opacity" Value="0.8" />
     <Setter Property="Opacity" Value="0.8" />
   </Style>
   </Style>
 
 

+ 28 - 0
src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml

@@ -0,0 +1,28 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+
+  <Styles.Resources>
+    <VisualBrush x:Key="CheckeredBackgroundBrush"
+                 TileMode="Tile"
+                 Stretch="Uniform"
+                 DestinationRect="0,0,8,8">
+      <VisualBrush.Visual>
+        <DrawingPresenter Width="8"
+                          Height="8">
+          <DrawingGroup>
+            <GeometryDrawing Geometry="M0,0 L2,0 2,2, 0,2Z"
+                             Brush="Transparent" />
+            <GeometryDrawing Geometry="M0,1 L2,1 2,2, 1,2 1,0 0,0Z"
+                             Brush="#19808080" />
+          </DrawingGroup>
+        </DrawingPresenter>
+      </VisualBrush.Visual>
+    </VisualBrush>
+  </Styles.Resources>
+
+  <!-- Primitives -->
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml" />
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml" />
+  <StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml" />
+
+</Styles>

+ 12 - 3
src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs

@@ -1,6 +1,5 @@
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
-
 using Avalonia.Data.Converters;
 using Avalonia.Data.Converters;
 
 
 namespace Avalonia.Controls.Converters
 namespace Avalonia.Controls.Converters
@@ -22,7 +21,12 @@ namespace Avalonia.Controls.Converters
         /// </summary>
         /// </summary>
         public double Scale { get; set; } = 1;
         public double Scale { get; set; } = 1;
 
 
-        public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+        /// <inheritdoc/>
+        public object? Convert(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
         {
         {
             if (!(value is CornerRadius radius))
             if (!(value is CornerRadius radius))
             {
             {
@@ -36,7 +40,12 @@ namespace Avalonia.Controls.Converters
                 Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0);
                 Filter.HasAllFlags(Corners.BottomLeft) ? radius.BottomLeft * Scale : 0);
         }
         }
 
 
-        public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+        /// <inheritdoc/>
+        public object? ConvertBack(
+            object? value,
+            Type targetType,
+            object? parameter,
+            CultureInfo culture)
         {
         {
             throw new NotImplementedException();
             throw new NotImplementedException();
         }
         }

+ 1 - 1
src/Avalonia.Controls/Viewbox.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Controls
         /// Defines the <see cref="Stretch"/> property.
         /// Defines the <see cref="Stretch"/> property.
         /// </summary>
         /// </summary>
         public static readonly StyledProperty<Stretch> StretchProperty =
         public static readonly StyledProperty<Stretch> StretchProperty =
-            AvaloniaProperty.Register<Image, Stretch>(nameof(Stretch), Stretch.Uniform);
+            AvaloniaProperty.Register<Viewbox, Stretch>(nameof(Stretch), Stretch.Uniform);
 
 
         /// <summary>
         /// <summary>
         /// Defines the <see cref="StretchDirection"/> property.
         /// Defines the <see cref="StretchDirection"/> property.

+ 7 - 3
src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml

@@ -1,9 +1,13 @@
 <Style xmlns="https://github.com/avaloniaui" 
 <Style xmlns="https://github.com/avaloniaui" 
-       Selector="DataValidationErrors">
+       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+       Selector="DataValidationErrors"
+       x:CompileBindings="True"
+       x:DataType="DataValidationErrors">
   <Setter Property="Template">
   <Setter Property="Template">
     <ControlTemplate>
     <ControlTemplate>
       <DockPanel LastChildFill="True">
       <DockPanel LastChildFill="True">
-        <ContentControl DockPanel.Dock="Right"
+        <ContentControl x:DataType="DataValidationErrors"
+                        DockPanel.Dock="Right"
                         ContentTemplate="{TemplateBinding ErrorTemplate}"
                         ContentTemplate="{TemplateBinding ErrorTemplate}"
                         DataContext="{TemplateBinding Owner}"
                         DataContext="{TemplateBinding Owner}"
                         Content="{Binding (DataValidationErrors.Errors)}"
                         Content="{Binding (DataValidationErrors.Errors)}"
@@ -30,7 +34,7 @@
           </Style>
           </Style>
         </Canvas.Styles>
         </Canvas.Styles>
         <ToolTip.Tip>
         <ToolTip.Tip>
-          <ItemsControl Items="{Binding}"/>
+          <ItemsControl x:DataType="DataValidationErrors" Items="{Binding}"/>
         </ToolTip.Tip>
         </ToolTip.Tip>
         <Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/>
         <Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/>
       </Canvas>
       </Canvas>

+ 8 - 10
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                 targetType = setter.Property.PropertyType;
                 targetType = setter.Property.PropertyType;
             }
             }
 
 
-            // Look upwards though the ambient context for IResourceHosts and IResourceProviders
+            // Look upwards though the ambient context for IResourceNodes
             // which might be able to give us the resource.
             // which might be able to give us the resource.
-            foreach (var e in stack.Parents)
+            foreach (var parent in stack.Parents)
             {
             {
-                object value;
-
-                if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value))
-                {
-                    return ColorToBrushConverter.Convert(value, targetType);
-                }
-                else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value))
+                if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value))
                 {
                 {
                     return ColorToBrushConverter.Convert(value, targetType);
                     return ColorToBrushConverter.Convert(value, targetType);
                 }
                 }
@@ -58,7 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
             if (provideTarget.TargetObject is IControl target &&
             if (provideTarget.TargetObject is IControl target &&
                 provideTarget.TargetProperty is PropertyInfo property)
                 provideTarget.TargetProperty is PropertyInfo property)
             {
             {
-                DelayedBinding.Add(target, property, x => GetValue(x, targetType));
+                // This is stored locally to avoid allocating closure in the outer scope.
+                var localTargetType = targetType;
+                var localInstance = this;
+                
+                DelayedBinding.Add(target, property, x => localInstance.GetValue(x, localTargetType));
                 return AvaloniaProperty.UnsetValue;
                 return AvaloniaProperty.UnsetValue;
             }
             }
 
 

+ 3 - 3
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@@ -60,7 +60,7 @@ namespace Avalonia.Markup.Xaml.Styling
             }
             }
         }
         }
 
 
-        bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false;
+        bool IResourceNode.HasResources => Loaded?.HasResources ?? false;
 
 
         IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
         IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
 
 
@@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling
 
 
         public bool TryGetResource(object key, out object? value)
         public bool TryGetResource(object key, out object? value)
         {
         {
-            if (!_isLoading && Loaded is IResourceProvider p)
+            if (!_isLoading)
             {
             {
-                return p.TryGetResource(key, out value);
+                return Loaded.TryGetResource(key, out value);
             }
             }
 
 
             value = null;
             value = null;