51 changed files with 3122 additions and 654 deletions
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,629 @@ |
|||||
|
// This source file is adapted from the WinUI project.
|
||||
|
// (https://github.com/microsoft/microsoft-ui-xaml)
|
||||
|
//
|
||||
|
// Licensed to The Avalonia Project under the MIT License.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Controls.Primitives |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Contains internal, special-purpose helpers used with the color picker.
|
||||
|
/// </summary>
|
||||
|
internal static class ColorPickerHelpers |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Generates a new bitmap of the specified size by changing a specific color component.
|
||||
|
/// This will produce a gradient representing a sweep of all possible values of the color component.
|
||||
|
/// </summary>
|
||||
|
/// <param name="width">The pixel width (X, horizontal) of the resulting bitmap.</param>
|
||||
|
/// <param name="height">The pixel height (Y, vertical) of the resulting bitmap.</param>
|
||||
|
/// <param name="orientation">The orientation of the resulting bitmap (gradient direction).</param>
|
||||
|
/// <param name="colorModel">The color model being used: RGBA or HSVA.</param>
|
||||
|
/// <param name="component">The specific color component to sweep.</param>
|
||||
|
/// <param name="baseHsvColor">The base HSV color used for components not being changed.</param>
|
||||
|
/// <param name="isAlphaMaxForced">Fix the alpha component value to maximum during calculation.
|
||||
|
/// This will remove any alpha/transparency from the other component backgrounds.</param>
|
||||
|
/// <param name="isSaturationValueMaxForced">Fix the saturation and value components to maximum
|
||||
|
/// during calculation with the HSVA color model.
|
||||
|
/// This will ensure colors are always discernible regardless of saturation/value.</param>
|
||||
|
/// <returns>A new bitmap representing a gradient of color component values.</returns>
|
||||
|
public static async Task<byte[]> CreateComponentBitmapAsync( |
||||
|
int width, |
||||
|
int height, |
||||
|
Orientation orientation, |
||||
|
ColorModel colorModel, |
||||
|
ColorComponent component, |
||||
|
HsvColor baseHsvColor, |
||||
|
bool isAlphaMaxForced, |
||||
|
bool isSaturationValueMaxForced) |
||||
|
{ |
||||
|
if (width == 0 || height == 0) |
||||
|
{ |
||||
|
return new byte[0]; |
||||
|
} |
||||
|
|
||||
|
var bitmap = await Task.Run<byte[]>(() => |
||||
|
{ |
||||
|
int pixelDataIndex = 0; |
||||
|
double componentStep; |
||||
|
byte[] bgraPixelData; |
||||
|
Color baseRgbColor = Colors.White; |
||||
|
Color rgbColor; |
||||
|
int bgraPixelDataHeight; |
||||
|
int bgraPixelDataWidth; |
||||
|
|
||||
|
// Allocate the buffer
|
||||
|
// BGRA formatted color components 1 byte each (4 bytes in a pixel)
|
||||
|
bgraPixelData = new byte[width * height * 4]; |
||||
|
bgraPixelDataHeight = height * 4; |
||||
|
bgraPixelDataWidth = width * 4; |
||||
|
|
||||
|
// Maximize alpha component value
|
||||
|
if (isAlphaMaxForced && |
||||
|
component != ColorComponent.Alpha) |
||||
|
{ |
||||
|
baseHsvColor = new HsvColor(1.0, baseHsvColor.H, baseHsvColor.S, baseHsvColor.V); |
||||
|
} |
||||
|
|
||||
|
// Convert HSV to RGB once
|
||||
|
if (colorModel == ColorModel.Rgba) |
||||
|
{ |
||||
|
baseRgbColor = baseHsvColor.ToRgb(); |
||||
|
} |
||||
|
|
||||
|
// Maximize Saturation and Value components when in HSVA mode
|
||||
|
if (isSaturationValueMaxForced && |
||||
|
colorModel == ColorModel.Hsva && |
||||
|
component != ColorComponent.Alpha) |
||||
|
{ |
||||
|
switch (component) |
||||
|
{ |
||||
|
case ColorComponent.Component1: |
||||
|
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, 1.0); |
||||
|
break; |
||||
|
case ColorComponent.Component2: |
||||
|
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, baseHsvColor.S, 1.0); |
||||
|
break; |
||||
|
case ColorComponent.Component3: |
||||
|
baseHsvColor = new HsvColor(baseHsvColor.A, baseHsvColor.H, 1.0, baseHsvColor.V); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Create the color component gradient
|
||||
|
if (orientation == Orientation.Horizontal) |
||||
|
{ |
||||
|
// Determine the numerical increment of the color steps within the component
|
||||
|
if (colorModel == ColorModel.Hsva) |
||||
|
{ |
||||
|
if (component == ColorComponent.Component1) |
||||
|
{ |
||||
|
componentStep = 360.0 / width; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
componentStep = 1.0 / width; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
componentStep = 255.0 / width; |
||||
|
} |
||||
|
|
||||
|
for (int y = 0; y < height; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < width; x++) |
||||
|
{ |
||||
|
if (y == 0) |
||||
|
{ |
||||
|
rgbColor = GetColor(x * componentStep); |
||||
|
|
||||
|
// Get a new color
|
||||
|
bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); |
||||
|
bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); |
||||
|
bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); |
||||
|
bgraPixelData[pixelDataIndex + 3] = rgbColor.A; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Use the color in the row above
|
||||
|
// Remember the pixel data is 1 dimensional instead of 2
|
||||
|
bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex + 0 - bgraPixelDataWidth]; |
||||
|
bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex + 1 - bgraPixelDataWidth]; |
||||
|
bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex + 2 - bgraPixelDataWidth]; |
||||
|
bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex + 3 - bgraPixelDataWidth]; |
||||
|
} |
||||
|
|
||||
|
pixelDataIndex += 4; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Determine the numerical increment of the color steps within the component
|
||||
|
if (colorModel == ColorModel.Hsva) |
||||
|
{ |
||||
|
if (component == ColorComponent.Component1) |
||||
|
{ |
||||
|
componentStep = 360.0 / height; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
componentStep = 1.0 / height; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
componentStep = 255.0 / height; |
||||
|
} |
||||
|
|
||||
|
for (int y = 0; y < height; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < width; x++) |
||||
|
{ |
||||
|
if (x == 0) |
||||
|
{ |
||||
|
// The lowest component value should be at the 'bottom' of the bitmap
|
||||
|
rgbColor = GetColor((height - 1 - y) * componentStep); |
||||
|
|
||||
|
// Get a new color
|
||||
|
bgraPixelData[pixelDataIndex + 0] = Convert.ToByte(rgbColor.B * rgbColor.A / 255); |
||||
|
bgraPixelData[pixelDataIndex + 1] = Convert.ToByte(rgbColor.G * rgbColor.A / 255); |
||||
|
bgraPixelData[pixelDataIndex + 2] = Convert.ToByte(rgbColor.R * rgbColor.A / 255); |
||||
|
bgraPixelData[pixelDataIndex + 3] = rgbColor.A; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Use the color in the column to the left
|
||||
|
// Remember the pixel data is 1 dimensional instead of 2
|
||||
|
bgraPixelData[pixelDataIndex + 0] = bgraPixelData[pixelDataIndex - 4]; |
||||
|
bgraPixelData[pixelDataIndex + 1] = bgraPixelData[pixelDataIndex - 3]; |
||||
|
bgraPixelData[pixelDataIndex + 2] = bgraPixelData[pixelDataIndex - 2]; |
||||
|
bgraPixelData[pixelDataIndex + 3] = bgraPixelData[pixelDataIndex - 1]; |
||||
|
} |
||||
|
|
||||
|
pixelDataIndex += 4; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Color GetColor(double componentValue) |
||||
|
{ |
||||
|
Color newRgbColor = Colors.White; |
||||
|
|
||||
|
switch (component) |
||||
|
{ |
||||
|
case ColorComponent.Component1: |
||||
|
{ |
||||
|
if (colorModel == ColorModel.Hsva) |
||||
|
{ |
||||
|
// Sweep hue
|
||||
|
newRgbColor = HsvColor.ToRgb( |
||||
|
MathUtilities.Clamp(componentValue, 0.0, 360.0), |
||||
|
baseHsvColor.S, |
||||
|
baseHsvColor.V, |
||||
|
baseHsvColor.A); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Sweep red
|
||||
|
newRgbColor = new Color( |
||||
|
baseRgbColor.A, |
||||
|
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), |
||||
|
baseRgbColor.G, |
||||
|
baseRgbColor.B); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case ColorComponent.Component2: |
||||
|
{ |
||||
|
if (colorModel == ColorModel.Hsva) |
||||
|
{ |
||||
|
// Sweep saturation
|
||||
|
newRgbColor = HsvColor.ToRgb( |
||||
|
baseHsvColor.H, |
||||
|
MathUtilities.Clamp(componentValue, 0.0, 1.0), |
||||
|
baseHsvColor.V, |
||||
|
baseHsvColor.A); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Sweep green
|
||||
|
newRgbColor = new Color( |
||||
|
baseRgbColor.A, |
||||
|
baseRgbColor.R, |
||||
|
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), |
||||
|
baseRgbColor.B); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case ColorComponent.Component3: |
||||
|
{ |
||||
|
if (colorModel == ColorModel.Hsva) |
||||
|
{ |
||||
|
// Sweep value
|
||||
|
newRgbColor = HsvColor.ToRgb( |
||||
|
baseHsvColor.H, |
||||
|
baseHsvColor.S, |
||||
|
MathUtilities.Clamp(componentValue, 0.0, 1.0), |
||||
|
baseHsvColor.A); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Sweep blue
|
||||
|
newRgbColor = new Color( |
||||
|
baseRgbColor.A, |
||||
|
baseRgbColor.R, |
||||
|
baseRgbColor.G, |
||||
|
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0))); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
case ColorComponent.Alpha: |
||||
|
{ |
||||
|
if (colorModel == ColorModel.Hsva) |
||||
|
{ |
||||
|
// Sweep alpha
|
||||
|
newRgbColor = HsvColor.ToRgb( |
||||
|
baseHsvColor.H, |
||||
|
baseHsvColor.S, |
||||
|
baseHsvColor.V, |
||||
|
MathUtilities.Clamp(componentValue, 0.0, 1.0)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Sweep alpha
|
||||
|
newRgbColor = new Color( |
||||
|
Convert.ToByte(MathUtilities.Clamp(componentValue, 0.0, 255.0)), |
||||
|
baseRgbColor.R, |
||||
|
baseRgbColor.G, |
||||
|
baseRgbColor.B); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return newRgbColor; |
||||
|
} |
||||
|
|
||||
|
return bgraPixelData; |
||||
|
}); |
||||
|
|
||||
|
return bitmap; |
||||
|
} |
||||
|
|
||||
|
public static Hsv IncrementColorComponent( |
||||
|
Hsv originalHsv, |
||||
|
HsvComponent component, |
||||
|
IncrementDirection direction, |
||||
|
IncrementAmount amount, |
||||
|
bool shouldWrap, |
||||
|
double minBound, |
||||
|
double maxBound) |
||||
|
{ |
||||
|
Hsv newHsv = originalHsv; |
||||
|
|
||||
|
if (amount == IncrementAmount.Small || !ColorHelper.ToDisplayNameExists) |
||||
|
{ |
||||
|
// In order to avoid working with small values that can incur rounding issues,
|
||||
|
// we'll multiple saturation and value by 100 to put them in the range of 0-100 instead of 0-1.
|
||||
|
newHsv.S *= 100; |
||||
|
newHsv.V *= 100; |
||||
|
|
||||
|
// Note: *valueToIncrement replaced with ref local variable for C#, must be initialized
|
||||
|
ref double valueToIncrement = ref newHsv.H; |
||||
|
double incrementAmount = 0.0; |
||||
|
|
||||
|
// If we're adding a small increment, then we'll just add or subtract 1.
|
||||
|
// If we're adding a large increment, then we want to snap to the next
|
||||
|
// or previous major value - for hue, this is every increment of 30;
|
||||
|
// for saturation and value, this is every increment of 10.
|
||||
|
switch (component) |
||||
|
{ |
||||
|
case HsvComponent.Hue: |
||||
|
valueToIncrement = ref newHsv.H; |
||||
|
incrementAmount = amount == IncrementAmount.Small ? 1 : 30; |
||||
|
break; |
||||
|
|
||||
|
case HsvComponent.Saturation: |
||||
|
valueToIncrement = ref newHsv.S; |
||||
|
incrementAmount = amount == IncrementAmount.Small ? 1 : 10; |
||||
|
break; |
||||
|
|
||||
|
case HsvComponent.Value: |
||||
|
valueToIncrement = ref newHsv.V; |
||||
|
incrementAmount = amount == IncrementAmount.Small ? 1 : 10; |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
throw new InvalidOperationException("Invalid HsvComponent."); |
||||
|
} |
||||
|
|
||||
|
double previousValue = valueToIncrement; |
||||
|
|
||||
|
valueToIncrement += (direction == IncrementDirection.Lower ? -incrementAmount : incrementAmount); |
||||
|
|
||||
|
// If the value has reached outside the bounds, we were previous at the boundary, and we should wrap,
|
||||
|
// then we'll place the selection on the other side of the spectrum.
|
||||
|
// Otherwise, we'll place it on the boundary that was exceeded.
|
||||
|
if (valueToIncrement < minBound) |
||||
|
{ |
||||
|
valueToIncrement = (shouldWrap && previousValue == minBound) ? maxBound : minBound; |
||||
|
} |
||||
|
|
||||
|
if (valueToIncrement > maxBound) |
||||
|
{ |
||||
|
valueToIncrement = (shouldWrap && previousValue == maxBound) ? minBound : maxBound; |
||||
|
} |
||||
|
|
||||
|
// We multiplied saturation and value by 100 previously, so now we want to put them back in the 0-1 range.
|
||||
|
newHsv.S /= 100; |
||||
|
newHsv.V /= 100; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// While working with named colors, we're going to need to be working in actual HSV units,
|
||||
|
// so we'll divide the min bound and max bound by 100 in the case of saturation or value,
|
||||
|
// since we'll have received units between 0-100 and we need them within 0-1.
|
||||
|
if (component == HsvComponent.Saturation || |
||||
|
component == HsvComponent.Value) |
||||
|
{ |
||||
|
minBound /= 100; |
||||
|
maxBound /= 100; |
||||
|
} |
||||
|
|
||||
|
newHsv = FindNextNamedColor(originalHsv, component, direction, shouldWrap, minBound, maxBound); |
||||
|
} |
||||
|
|
||||
|
return newHsv; |
||||
|
} |
||||
|
|
||||
|
public static Hsv FindNextNamedColor( |
||||
|
Hsv originalHsv, |
||||
|
HsvComponent component, |
||||
|
IncrementDirection direction, |
||||
|
bool shouldWrap, |
||||
|
double minBound, |
||||
|
double maxBound) |
||||
|
{ |
||||
|
// There's no easy way to directly get the next named color, so what we'll do
|
||||
|
// is just iterate in the direction that we want to find it until we find a color
|
||||
|
// in that direction that has a color name different than our current color name.
|
||||
|
// Once we find a new color name, then we'll iterate across that color name until
|
||||
|
// we find its bounds on the other side, and then select the color that is exactly
|
||||
|
// in the middle of that color's bounds.
|
||||
|
Hsv newHsv = originalHsv; |
||||
|
|
||||
|
string originalColorName = ColorHelper.ToDisplayName(originalHsv.ToRgb().ToColor()); |
||||
|
string newColorName = originalColorName; |
||||
|
|
||||
|
// Note: *newValue replaced with ref local variable for C#, must be initialized
|
||||
|
double originalValue = 0.0; |
||||
|
ref double newValue = ref newHsv.H; |
||||
|
double incrementAmount = 0.0; |
||||
|
|
||||
|
switch (component) |
||||
|
{ |
||||
|
case HsvComponent.Hue: |
||||
|
originalValue = originalHsv.H; |
||||
|
newValue = ref newHsv.H; |
||||
|
incrementAmount = 1; |
||||
|
break; |
||||
|
|
||||
|
case HsvComponent.Saturation: |
||||
|
originalValue = originalHsv.S; |
||||
|
newValue = ref newHsv.S; |
||||
|
incrementAmount = 0.01; |
||||
|
break; |
||||
|
|
||||
|
case HsvComponent.Value: |
||||
|
originalValue = originalHsv.V; |
||||
|
newValue = ref newHsv.V; |
||||
|
incrementAmount = 0.01; |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
throw new InvalidOperationException("Invalid HsvComponent."); |
||||
|
} |
||||
|
|
||||
|
bool shouldFindMidPoint = true; |
||||
|
|
||||
|
while (newColorName == originalColorName) |
||||
|
{ |
||||
|
double previousValue = newValue; |
||||
|
newValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; |
||||
|
|
||||
|
bool justWrapped = false; |
||||
|
|
||||
|
// If we've hit a boundary, then either we should wrap or we shouldn't.
|
||||
|
// If we should, then we'll perform that wrapping if we were previously up against
|
||||
|
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
|
||||
|
if (newValue > maxBound) |
||||
|
{ |
||||
|
if (shouldWrap) |
||||
|
{ |
||||
|
newValue = minBound; |
||||
|
justWrapped = true; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
newValue = maxBound; |
||||
|
shouldFindMidPoint = false; |
||||
|
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
else if (newValue < minBound) |
||||
|
{ |
||||
|
if (shouldWrap) |
||||
|
{ |
||||
|
newValue = maxBound; |
||||
|
justWrapped = true; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
newValue = minBound; |
||||
|
shouldFindMidPoint = false; |
||||
|
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!justWrapped && |
||||
|
previousValue != originalValue && |
||||
|
Math.Sign(newValue - originalValue) != Math.Sign(previousValue - originalValue)) |
||||
|
{ |
||||
|
// If we've wrapped all the way back to the start and have failed to find a new color name,
|
||||
|
// then we'll just quit - there isn't a new color name that we're going to find.
|
||||
|
shouldFindMidPoint = false; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
newColorName = ColorHelper.ToDisplayName(newHsv.ToRgb().ToColor()); |
||||
|
} |
||||
|
|
||||
|
if (shouldFindMidPoint) |
||||
|
{ |
||||
|
Hsv startHsv = newHsv; |
||||
|
Hsv currentHsv = startHsv; |
||||
|
double startEndOffset = 0; |
||||
|
string currentColorName = newColorName; |
||||
|
|
||||
|
// Note: *startValue/*currentValue replaced with ref local variables for C#, must be initialized
|
||||
|
ref double startValue = ref startHsv.H; |
||||
|
ref double currentValue = ref currentHsv.H; |
||||
|
double wrapIncrement = 0; |
||||
|
|
||||
|
switch (component) |
||||
|
{ |
||||
|
case HsvComponent.Hue: |
||||
|
startValue = ref startHsv.H; |
||||
|
currentValue = ref currentHsv.H; |
||||
|
wrapIncrement = 360.0; |
||||
|
break; |
||||
|
|
||||
|
case HsvComponent.Saturation: |
||||
|
startValue = ref startHsv.S; |
||||
|
currentValue = ref currentHsv.S; |
||||
|
wrapIncrement = 1.0; |
||||
|
break; |
||||
|
|
||||
|
case HsvComponent.Value: |
||||
|
startValue = ref startHsv.V; |
||||
|
currentValue = ref currentHsv.V; |
||||
|
wrapIncrement = 1.0; |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
throw new InvalidOperationException("Invalid HsvComponent."); |
||||
|
} |
||||
|
|
||||
|
while (newColorName == currentColorName) |
||||
|
{ |
||||
|
currentValue += (direction == IncrementDirection.Lower ? -1 : 1) * incrementAmount; |
||||
|
|
||||
|
// If we've hit a boundary, then either we should wrap or we shouldn't.
|
||||
|
// If we should, then we'll perform that wrapping if we were previously up against
|
||||
|
// the boundary that we've now hit. Otherwise, we'll stop at that boundary.
|
||||
|
if (currentValue > maxBound) |
||||
|
{ |
||||
|
if (shouldWrap) |
||||
|
{ |
||||
|
currentValue = minBound; |
||||
|
startEndOffset = maxBound - minBound; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
currentValue = maxBound; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
else if (currentValue < minBound) |
||||
|
{ |
||||
|
if (shouldWrap) |
||||
|
{ |
||||
|
currentValue = maxBound; |
||||
|
startEndOffset = minBound - maxBound; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
currentValue = minBound; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
currentColorName = ColorHelper.ToDisplayName(currentHsv.ToRgb().ToColor()); |
||||
|
} |
||||
|
|
||||
|
newValue = (startValue + currentValue + startEndOffset) / 2; |
||||
|
|
||||
|
// Dividing by 2 may have gotten us halfway through a single step, so we'll
|
||||
|
// remove that half-step if it exists.
|
||||
|
double leftoverValue = Math.Abs(newValue); |
||||
|
|
||||
|
while (leftoverValue > incrementAmount) |
||||
|
{ |
||||
|
leftoverValue -= incrementAmount; |
||||
|
} |
||||
|
|
||||
|
newValue -= leftoverValue; |
||||
|
|
||||
|
while (newValue < minBound) |
||||
|
{ |
||||
|
newValue += wrapIncrement; |
||||
|
} |
||||
|
|
||||
|
while (newValue > maxBound) |
||||
|
{ |
||||
|
newValue -= wrapIncrement; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return newHsv; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts the given raw BGRA pre-multiplied alpha pixel data into a bitmap.
|
||||
|
/// </summary>
|
||||
|
/// <param name="bgraPixelData">The bitmap (in raw BGRA pre-multiplied alpha pixels).</param>
|
||||
|
/// <param name="pixelWidth">The pixel width of the bitmap.</param>
|
||||
|
/// <param name="pixelHeight">The pixel height of the bitmap.</param>
|
||||
|
/// <returns>A new <see cref="WriteableBitmap"/>.</returns>
|
||||
|
public static WriteableBitmap CreateBitmapFromPixelData( |
||||
|
IList<byte> bgraPixelData, |
||||
|
int pixelWidth, |
||||
|
int pixelHeight) |
||||
|
{ |
||||
|
// Standard may need to change on some devices
|
||||
|
Vector dpi = new Vector(96, 96); |
||||
|
|
||||
|
var bitmap = new WriteableBitmap( |
||||
|
new PixelSize(pixelWidth, pixelHeight), |
||||
|
dpi, |
||||
|
PixelFormat.Bgra8888, |
||||
|
AlphaFormat.Premul); |
||||
|
|
||||
|
// Warning: This is highly questionable
|
||||
|
using (var frameBuffer = bitmap.Lock()) |
||||
|
{ |
||||
|
Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); |
||||
|
} |
||||
|
|
||||
|
return bitmap; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +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 |
||||
|
}; |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue