Browse Source

Merge branch 'master' into devtools-crash-exceptions

pull/8067/head
Dariusz Komosiński 4 years ago
committed by GitHub
parent
commit
225b193ee8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/ControlCatalog/App.xaml.cs
  2. 86
      samples/ControlCatalog/Pages/ColorPickerPage.xaml
  3. 11
      src/Avalonia.Base/Controls/ResourceNodeExtensions.cs
  4. 2
      src/Avalonia.Base/Styling/IStyle.cs
  5. 2
      src/Avalonia.Base/Styling/Styles.cs
  6. 2
      src/Avalonia.Base/Utilities/WeakHashList.cs
  7. 28
      src/Avalonia.Controls.ColorPicker/ColorComponent.cs
  8. 18
      src/Avalonia.Controls.ColorPicker/ColorModel.cs
  9. 50
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.Properties.cs
  10. 130
      src/Avalonia.Controls.ColorPicker/ColorPreviewer/ColorPreviewer.cs
  11. 146
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.Properties.cs
  12. 399
      src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs
  13. 414
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorHelpers.cs
  14. 151
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs
  15. 138
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
  16. 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumComponents.cs
  17. 0
      src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrumShape.cs
  18. 116
      src/Avalonia.Controls.ColorPicker/Converters/AccentColorConverter.cs
  19. 68
      src/Avalonia.Controls.ColorPicker/Converters/ColorToDisplayNameConverter.cs
  20. 82
      src/Avalonia.Controls.ColorPicker/Converters/ColorToHexConverter.cs
  21. 51
      src/Avalonia.Controls.ColorPicker/Converters/ThirdComponentConverter.cs
  22. 50
      src/Avalonia.Controls.ColorPicker/Converters/ToBrushConverter.cs
  23. 58
      src/Avalonia.Controls.ColorPicker/Converters/ToColorConverter.cs
  24. 50
      src/Avalonia.Controls.ColorPicker/Converters/ValueConverterGroup.cs
  25. 142
      src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs
  26. 629
      src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs
  27. 0
      src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs
  28. 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementAmount.cs
  29. 0
      src/Avalonia.Controls.ColorPicker/Helpers/IncrementDirection.cs
  30. 0
      src/Avalonia.Controls.ColorPicker/Helpers/Rgb.cs
  31. 22
      src/Avalonia.Controls.ColorPicker/HsvComponent.cs
  32. 42
      src/Avalonia.Controls.ColorPicker/RgbComponent.cs
  33. 86
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorPreviewer.xaml
  34. 194
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSlider.xaml
  35. 22
      src/Avalonia.Controls.ColorPicker/Themes/Default/ColorSpectrum.xaml
  36. 28
      src/Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml
  37. 86
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPreviewer.xaml
  38. 194
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSlider.xaml
  39. 25
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorSpectrum.xaml
  40. 28
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml
  41. 15
      src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs
  42. 10
      src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml
  43. 3
      src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml
  44. 28
      src/Avalonia.Themes.Default/IBitmapToImageConverter.cs
  45. 7
      src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml
  46. 28
      src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs
  47. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  48. 6
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

4
samples/ControlCatalog/App.xaml.cs

@ -20,12 +20,12 @@ namespace ControlCatalog
public static readonly StyleInclude ColorPickerFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent.xaml")
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml")
};
public static readonly StyleInclude ColorPickerDefault = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
{
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default.xaml")
Source = new Uri("avares://Avalonia.Controls.ColorPicker/Themes/Default/Default.xaml")
};
public static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))

86
samples/ControlCatalog/Pages/ColorPickerPage.xaml

@ -3,27 +3,77 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:primitives="clr-namespace:Avalonia.Controls.Primitives;assembly=Avalonia.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:pc="clr-namespace:Avalonia.Controls.Primitives.Converters;assembly=Avalonia.Controls.ColorPicker"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="ControlCatalog.Pages.ColorPickerPage">
<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"
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"
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>
</UserControl>

11
src/Avalonia.Base/Controls/ResourceNodeExtensions.cs

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

2
src/Avalonia.Base/Styling/IStyle.cs

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

2
src/Avalonia.Base/Styling/Styles.cs

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

2
src/Avalonia.Base/Utilities/WeakHashList.cs

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

28
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
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
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
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
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
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);
}
}
}

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);
}
}
}

151
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.Properties.cs

@ -3,6 +3,7 @@
//
// Licensed to The Avalonia Project under the MIT License.
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Controls.Primitives
@ -10,6 +11,88 @@ namespace Avalonia.Controls.Primitives
/// <inheritdoc/>
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>
/// Gets or sets the currently selected color in the RGB color model.
/// </summary>
@ -23,14 +106,6 @@ namespace Avalonia.Controls.Primitives
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>
/// Gets or sets the two HSV color components displayed by the spectrum.
/// </summary>
@ -43,14 +118,6 @@ namespace Avalonia.Controls.Primitives
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>
/// Gets or sets the currently selected color in the HSV color model.
/// </summary>
@ -65,14 +132,6 @@ namespace Avalonia.Controls.Primitives
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>
/// 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"/>.
@ -86,12 +145,6 @@ namespace Avalonia.Controls.Primitives
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>
/// 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"/>.
@ -105,12 +158,6 @@ namespace Avalonia.Controls.Primitives
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>
/// 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"/>.
@ -124,12 +171,6 @@ namespace Avalonia.Controls.Primitives
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>
/// 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"/>.
@ -143,12 +184,6 @@ namespace Avalonia.Controls.Primitives
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>
/// 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"/>.
@ -162,12 +197,6 @@ namespace Avalonia.Controls.Primitives
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>
/// 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"/>.
@ -181,12 +210,6 @@ namespace Avalonia.Controls.Primitives
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>
/// Gets or sets the displayed shape of the spectrum.
/// </summary>
@ -195,13 +218,5 @@ namespace Avalonia.Controls.Primitives
get => GetValue(ShapeProperty);
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);
}
}

138
src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
@ -20,7 +21,6 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// A two dimensional spectrum for color selection.
/// </summary>
[TemplatePart("PART_ColorNameToolTip", typeof(ToolTip))]
[TemplatePart("PART_InputTarget", typeof(Canvas))]
[TemplatePart("PART_LayoutRoot", typeof(Panel))]
[TemplatePart("PART_SelectionEllipsePanel", typeof(Panel))]
@ -29,10 +29,11 @@ namespace Avalonia.Controls.Primitives
[TemplatePart("PART_SpectrumRectangle", typeof(Rectangle))]
[TemplatePart("PART_SpectrumOverlayEllipse", typeof(Ellipse))]
[TemplatePart("PART_SpectrumOverlayRectangle", typeof(Rectangle))]
[PseudoClasses(pcPressed, pcLargeSelector, pcLightSelector)]
[PseudoClasses(pcPressed, pcLargeSelector, pcDarkSelector, pcLightSelector)]
public partial class ColorSpectrum : TemplatedControl
{
protected const string pcPressed = ":pressed";
protected const string pcDarkSelector = ":dark-selector";
protected const string pcLargeSelector = ":large-selector";
protected const string pcLightSelector = ":light-selector";
@ -60,7 +61,6 @@ namespace Avalonia.Controls.Primitives
private Ellipse? _spectrumOverlayEllipse;
private Canvas? _inputTarget;
private Panel? _selectionEllipsePanel;
private ToolTip? _colorNameToolTip;
// Put the spectrum images in a bitmap, which is then given to an ImageBrush.
private WriteableBitmap? _hueRedBitmap;
@ -117,7 +117,6 @@ namespace Avalonia.Controls.Primitives
UnregisterEvents(); // Failsafe
_colorNameToolTip = e.NameScope.Find<ToolTip>("PART_ColorNameToolTip");
_inputTarget = e.NameScope.Find<Canvas>("PART_InputTarget");
_layoutRoot = e.NameScope.Find<Panel>("PART_LayoutRoot");
_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.
@ -320,7 +319,7 @@ namespace Avalonia.Controls.Primitives
IncrementAmount amount = isControlDown ? IncrementAmount.Large : IncrementAmount.Small;
HsvColor hsvColor = HsvColor;
UpdateColor(ColorHelpers.IncrementColorComponent(
UpdateColor(ColorPickerHelpers.IncrementColorComponent(
new Hsv(hsvColor),
incrementComponent,
direction,
@ -330,34 +329,51 @@ namespace Avalonia.Controls.Primitives
maxBound));
e.Handled = true;
return;
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
// We only want to bother with the color name tool tip if we can provide color names.
if (_colorNameToolTip != null &&
ColorHelpers.ToDisplayNameExists)
if (_selectionEllipsePanel != null &&
ColorHelper.ToDisplayNameExists)
{
ToolTip.SetIsOpen(_colorNameToolTip, true);
ToolTip.SetIsOpen(_selectionEllipsePanel, true);
}
UpdatePseudoClasses();
base.OnGotFocus(e);
}
/// <inheritdoc/>
protected override void OnLostFocus(RoutedEventArgs e)
{
// We only want to bother with the color name tool tip if we can provide color names.
if (_colorNameToolTip != null &&
ColorHelpers.ToDisplayNameExists)
if (_selectionEllipsePanel != null &&
ColorHelper.ToDisplayNameExists)
{
ToolTip.SetIsOpen(_colorNameToolTip, false);
ToolTip.SetIsOpen(_selectionEllipsePanel, false);
}
UpdatePseudoClasses();
base.OnLostFocus(e);
}
/// <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/>
@ -516,12 +532,10 @@ namespace Avalonia.Controls.Primitives
var colorChangedEventArgs = new ColorChangedEventArgs(_oldColor, newColor);
ColorChanged?.Invoke(this, colorChangedEventArgs);
if (ColorHelpers.ToDisplayNameExists)
if (_selectionEllipsePanel != null &&
ColorHelper.ToDisplayNameExists)
{
if (_colorNameToolTip != null)
{
_colorNameToolTip.Content = ColorHelpers.ToDisplayName(newColor);
}
ToolTip.SetTip(_selectionEllipsePanel, ColorHelper.ToDisplayName(Color));
}
}
}
@ -543,7 +557,16 @@ namespace Avalonia.Controls.Primitives
PseudoClasses.Set(pcLargeSelector, false);
}
PseudoClasses.Set(pcLightSelector, SelectionEllipseShouldBeLight());
if (SelectionEllipseShouldBeLight())
{
PseudoClasses.Set(pcDarkSelector, false);
PseudoClasses.Set(pcLightSelector, true);
}
else
{
PseudoClasses.Set(pcDarkSelector, true);
PseudoClasses.Set(pcLightSelector, false);
}
}
private void UpdateColor(Hsv newHsv)
@ -575,8 +598,10 @@ namespace Avalonia.Controls.Primitives
return;
}
double xPosition = point.Position.X;
double yPosition = point.Position.Y;
// Remember the bitmap size follows physical device pixels
var scale = LayoutHelper.GetLayoutScale(this);
double xPosition = point.Position.X * scale;
double yPosition = point.Position.Y * scale;
double radius = Math.Min(_imageWidthFromLastBitmapCreation, _imageHeightFromLastBitmapCreation) / 2;
double distanceFromRadius = Math.Sqrt(Math.Pow(xPosition - radius, 2) + Math.Pow(yPosition - radius, 2));
@ -807,19 +832,17 @@ namespace Avalonia.Controls.Primitives
yPosition = (Math.Sin((thetaValue * Math.PI / 180.0) + Math.PI) * radius * rValue) + radius;
}
Canvas.SetLeft(_selectionEllipsePanel, xPosition - (_selectionEllipsePanel.Width / 2));
Canvas.SetTop(_selectionEllipsePanel, yPosition - (_selectionEllipsePanel.Height / 2));
// Remember the bitmap size follows physical device pixels
var scale = LayoutHelper.GetLayoutScale(this);
Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2));
Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2));
// We only want to bother with the color name tool tip if we can provide color names.
if (ColorHelpers.ToDisplayNameExists)
if (IsFocused &&
_selectionEllipsePanel != null &&
ColorHelper.ToDisplayNameExists)
{
if (_colorNameToolTip != null)
{
// ToolTip doesn't currently provide any way to re-run its placement logic if its placement target moves,
// so toggling IsEnabled induces it to do that without incurring any visual glitches.
_colorNameToolTip.IsEnabled = false;
_colorNameToolTip.IsEnabled = true;
}
ToolTip.SetIsOpen(_selectionEllipsePanel, true);
}
UpdatePseudoClasses();
@ -961,7 +984,14 @@ namespace Avalonia.Controls.Primitives
List<byte> bgraMaxPixelData = new List<byte>();
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;
bgraMinPixelData.Capacity = pixelDataSize;
@ -978,8 +1008,6 @@ namespace Avalonia.Controls.Primitives
bgraMaxPixelData.Capacity = pixelDataSize;
newHsvValues.Capacity = pixelCount;
int minDimensionInt = (int)Math.Round(minDimension);
await Task.Run(() =>
{
// As the user perceives it, every time the third dimension not represented in the ColorSpectrum changes,
@ -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.
if (shape == ColorSpectrumShape.Box)
{
for (int x = minDimensionInt - 1; x >= 0; --x)
for (int x = pixelDimension - 1; x >= 0; --x)
{
for (int y = minDimensionInt - 1; y >= 0; --y)
for (int y = pixelDimension - 1; y >= 0; --y)
{
FillPixelForBox(
x, y, hsv, minDimensionInt, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue,
x, y, hsv, pixelDimension, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue,
bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData,
newHsvValues);
}
@ -1011,12 +1039,12 @@ namespace Avalonia.Controls.Primitives
}
else
{
for (int y = 0; y < minDimensionInt; ++y)
for (int y = 0; y < pixelDimension; ++y)
{
for (int x = 0; x < minDimensionInt; ++x)
for (int x = 0; x < pixelDimension; ++x)
{
FillPixelForRing(
x, y, minDimensionInt / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue,
x, y, pixelDimension / 2.0, hsv, components, minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue,
bgraMinPixelData, bgraMiddle1PixelData, bgraMiddle2PixelData, bgraMiddle3PixelData, bgraMiddle4PixelData, bgraMaxPixelData,
newHsvValues);
}
@ -1026,13 +1054,13 @@ namespace Avalonia.Controls.Primitives
Dispatcher.UIThread.Post(() =>
{
int pixelWidth = (int)Math.Round(minDimension);
int pixelHeight = (int)Math.Round(minDimension);
int pixelWidth = pixelDimension;
int pixelHeight = pixelDimension;
ColorSpectrumComponents components2 = Components;
WriteableBitmap minBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMinPixelData);
WriteableBitmap maxBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMaxPixelData);
WriteableBitmap minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight);
WriteableBitmap maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight);
switch (components2)
{
@ -1048,18 +1076,18 @@ namespace Avalonia.Controls.Primitives
case ColorSpectrumComponents.ValueSaturation:
case ColorSpectrumComponents.SaturationValue:
_hueRedBitmap = minBitmap;
_hueYellowBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle1PixelData);
_hueGreenBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle2PixelData);
_hueCyanBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle3PixelData);
_hueBlueBitmap = ColorHelpers.CreateBitmapFromPixelData(pixelWidth, pixelHeight, bgraMiddle4PixelData);
_hueYellowBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle1PixelData, pixelWidth, pixelHeight);
_hueGreenBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle2PixelData, pixelWidth, pixelHeight);
_hueCyanBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle3PixelData, pixelWidth, pixelHeight);
_hueBlueBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle4PixelData, pixelWidth, pixelHeight);
_huePurpleBitmap = maxBitmap;
break;
}
_shapeFromLastBitmapCreation = Shape;
_componentsFromLastBitmapCreation = Components;
_imageWidthFromLastBitmapCreation = minDimension;
_imageHeightFromLastBitmapCreation = minDimension;
_imageWidthFromLastBitmapCreation = pixelDimension;
_imageHeightFromLastBitmapCreation = pixelDimension;
_minHueFromLastBitmapCreation = MinHue;
_maxHueFromLastBitmapCreation = MaxHue;
_minSaturationFromLastBitmapCreation = MinSaturation;
@ -1078,7 +1106,7 @@ namespace Avalonia.Controls.Primitives
double x,
double y,
Hsv baseHsv,
double minDimension,
int minDimension,
ColorSpectrumComponents components,
double minHue,
double maxHue,
@ -1570,7 +1598,7 @@ namespace Avalonia.Controls.Primitives
displayedColor = Color;
}
var lum = ColorHelpers.GetRelativeLuminance(displayedColor);
var lum = ColorHelper.GetRelativeLuminance(displayedColor);
return lum <= 0.5;
}

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

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

116
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
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
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
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
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
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
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
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
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
src/Avalonia.Controls.ColorPicker/ColorSpectrum/Hsv.cs → src/Avalonia.Controls.ColorPicker/Helpers/Hsv.cs

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

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

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

22
src/Avalonia.Controls.ColorPicker/HsvComponent.cs

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

42
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
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
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>

22
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"
x:CompileBindings="True"
xmlns:converters="using:Avalonia.Controls.Converters">
xmlns:converters="using:Avalonia.Controls.Converters"
x:CompileBindings="True">
<Styles.Resources>
<converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
@ -48,7 +48,10 @@
Background="Transparent"
HorizontalAlignment="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"
Margin="-2"
StrokeThickness="2"
@ -59,13 +62,10 @@
StrokeThickness="2"
IsHitTestVisible="False"
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>
</Canvas>
<Rectangle x:Name="BorderRectangle"

28
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
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
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>

25
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"
x:CompileBindings="True"
xmlns:converters="using:Avalonia.Controls.Converters">
xmlns:converters="using:Avalonia.Controls.Converters"
x:CompileBindings="True">
<Styles.Resources>
<converters:EnumValueEqualsConverter x:Key="EnumValueEquals" />
@ -48,7 +48,10 @@
Background="Transparent"
HorizontalAlignment="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"
Margin="-2"
StrokeThickness="2"
@ -59,13 +62,10 @@
StrokeThickness="2"
IsHitTestVisible="False"
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>
</Canvas>
<Rectangle x:Name="BorderRectangle"
@ -118,6 +118,9 @@
</Style>
<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" />
</Style>

28
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>

15
src/Avalonia.Controls/Converters/CornerRadiusFilterConverter.cs

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

10
src/Avalonia.Themes.Default/Controls/DataValidationErrors.xaml

@ -1,9 +1,13 @@
<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">
<ControlTemplate>
<DockPanel LastChildFill="True">
<ContentControl DockPanel.Dock="Right"
<ContentControl x:DataType="DataValidationErrors"
DockPanel.Dock="Right"
ContentTemplate="{TemplateBinding ErrorTemplate}"
DataContext="{TemplateBinding Owner}"
Content="{Binding (DataValidationErrors.Errors)}"
@ -30,7 +34,7 @@
</Style>
</Canvas.Styles>
<ToolTip.Tip>
<ItemsControl Items="{Binding}"/>
<ItemsControl x:DataType="DataValidationErrors" Items="{Binding}"/>
</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"/>
</Canvas>

3
src/Avalonia.Themes.Default/Controls/NativeMenuBar.xaml

@ -4,6 +4,7 @@
Selector="NativeMenuBar">
<Style.Resources>
<local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/>
<local:IBitmapToImageConverter x:Key="AvaloniaThemesDefaultNativeMenuBarIBitmapToImageConverterr"/>
</Style.Resources>
<Setter Property="Template">
<ControlTemplate>
@ -18,6 +19,8 @@
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/>
<!--NativeMenuItem is IBitmap and MenuItem is Image-->
<Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarIBitmapToImageConverterr}}"/>
</Style>
</Menu.Styles>
</Menu>

28
src/Avalonia.Themes.Default/IBitmapToImageConverter.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
namespace Avalonia.Themes.Default
{
internal class IBitmapToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is IBitmap bm)
return new Image { Source=bm };
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

7
src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml

@ -4,12 +4,13 @@
x:CompileBindings="True"
Selector="NativeMenuBar">
<Style.Resources>
<local:InverseBooleanValueConverter x:Key="AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter" Default="True"/>
<local:InverseBooleanValueConverter x:Key="AvaloniaThemesFluentNativeMenuBarInverseBooleanValueConverter" Default="True"/>
<local:IBitmapToImageConverter x:Key="AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter"/>
</Style.Resources>
<Setter Property="Template">
<ControlTemplate>
<Menu
IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesDefaultNativeMenuBarInverseBooleanValueConverter}}"
IsVisible="{Binding $parent[TopLevel].(NativeMenu.IsNativeMenuExported), Converter={StaticResource AvaloniaThemesFluentNativeMenuBarInverseBooleanValueConverter}}"
Items="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">
<Menu.Styles>
<Style x:CompileBindings="False" Selector="MenuItem">
@ -19,6 +20,8 @@
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
<Setter Property="(NativeMenuBar.EnableMenuItemClickForwarding)" Value="True"/>
<!--NativeMenuItem is IBitmap and MenuItem is Image-->
<Setter Property="Icon" Value="{Binding Icon , Converter={StaticResource AvaloniaThemesFluentNativeMenuBarIBitmapToImageConverter}}"/>
</Style>
</Menu.Styles>
</Menu>

28
src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
namespace Avalonia.Themes.Fluent
{
internal class IBitmapToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && value is IBitmap bm)
return new Image { Source=bm };
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

18
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@ -39,17 +39,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
targetType = setter.Property.PropertyType;
}
// Look upwards though the ambient context for IResourceHosts and IResourceProviders
// Look upwards though the ambient context for IResourceNodes
// which might be able to give us the resource.
foreach (var e in stack.Parents)
foreach (var parent in stack.Parents)
{
object value;
if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value))
{
return ColorToBrushConverter.Convert(value, targetType);
}
else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value))
if (parent is IResourceNode node && node.TryGetResource(ResourceKey, out var value))
{
return ColorToBrushConverter.Convert(value, targetType);
}
@ -58,7 +52,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
if (provideTarget.TargetObject is IControl target &&
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;
}

6
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>();
@ -86,9 +86,9 @@ namespace Avalonia.Markup.Xaml.Styling
public bool TryGetResource(object key, out object? value)
{
if (!_isLoading && Loaded is IResourceProvider p)
if (!_isLoading)
{
return p.TryGetResource(key, out value);
return Loaded.TryGetResource(key, out value);
}
value = null;

Loading…
Cancel
Save