7 changed files with 925 additions and 2 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, |
|||
|
|||
/// <summary>
|
|||
/// Represents the first color component which is Red when RGB or Hue when HSV.
|
|||
/// </summary>
|
|||
Component1, |
|||
|
|||
/// <summary>
|
|||
/// Represents the second color component which is Green when RGB or Saturation when HSV.
|
|||
/// </summary>
|
|||
Component2, |
|||
|
|||
/// <summary>
|
|||
/// Represents the third color component which is Blue when RGB or Value when HSV.
|
|||
/// </summary>
|
|||
Component3 |
|||
} |
|||
} |
|||
@ -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,143 @@ |
|||
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); |
|||
|
|||
/// <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>
|
|||
/// Defines the <see cref="ColorComponent"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<ColorComponent> ColorComponentProperty = |
|||
AvaloniaProperty.Register<ColorSlider, ColorComponent>( |
|||
nameof(ColorComponent), |
|||
ColorComponent.Component1); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the color component represented by the slider.
|
|||
/// </summary>
|
|||
public ColorComponent ColorComponent |
|||
{ |
|||
get => GetValue(ColorComponentProperty); |
|||
set => SetValue(ColorComponentProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="ColorModel"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<ColorModel> ColorModelProperty = |
|||
AvaloniaProperty.Register<ColorSlider, ColorModel>( |
|||
nameof(ColorModel), |
|||
ColorModel.Rgba); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the active color model used by the slider.
|
|||
/// </summary>
|
|||
public ColorModel ColorModel |
|||
{ |
|||
get => GetValue(ColorModelProperty); |
|||
set => SetValue(ColorModelProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="HsvColor"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<HsvColor> HsvColorProperty = |
|||
AvaloniaProperty.Register<ColorSlider, HsvColor>( |
|||
nameof(HsvColor), |
|||
Colors.White.ToHsv()); |
|||
|
|||
/// <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>
|
|||
/// Defines the <see cref="IsAlphaMaxForced"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> IsAlphaMaxForcedProperty = |
|||
AvaloniaProperty.Register<ColorSlider, bool>( |
|||
nameof(IsAlphaMaxForced), |
|||
true); |
|||
|
|||
/// <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>
|
|||
/// Defines the <see cref="IsAutoUpdatingEnabled"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> IsAutoUpdatingEnabledProperty = |
|||
AvaloniaProperty.Register<ColorSlider, bool>( |
|||
nameof(IsAutoUpdatingEnabled), |
|||
true); |
|||
|
|||
/// <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>
|
|||
/// Defines the <see cref="IsSaturationValueMaxForced"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<bool> IsSaturationValueMaxForcedProperty = |
|||
AvaloniaProperty.Register<ColorSlider, bool>( |
|||
nameof(IsSaturationValueMaxForced), |
|||
true); |
|||
|
|||
/// <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,221 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// A slider with a background that represents a single color component.
|
|||
/// </summary>
|
|||
public partial class ColorSlider : Slider |
|||
{ |
|||
private Size cachedSize = Size.Empty; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ColorSlider"/> class.
|
|||
/// </summary>
|
|||
public ColorSlider() : base() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Update the slider's Foreground and Background brushes based on the current slider state and color.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Manually refreshes the background gradient of the slider.
|
|||
/// This is callable separately for performance reasons.
|
|||
/// </remarks>
|
|||
public void UpdateColors() |
|||
{ |
|||
HsvColor hsvColor = HsvColor; |
|||
|
|||
// Calculate and set the background
|
|||
UpdateBackground(hsvColor); |
|||
|
|||
// Calculate and set the foreground ensuring contrast with the background
|
|||
Color rgbColor = hsvColor.ToRgb(); |
|||
Color selectedRgbColor; |
|||
double sliderPercent = Value / (Maximum - Minimum); |
|||
|
|||
var component = ColorComponent; |
|||
|
|||
if (ColorModel == ColorModel.Hsva) |
|||
{ |
|||
if (IsAlphaMaxForced && |
|||
component != ColorComponent.Alpha) |
|||
{ |
|||
hsvColor = new HsvColor(1.0, hsvColor.H, hsvColor.S, hsvColor.V); |
|||
} |
|||
|
|||
switch (component) |
|||
{ |
|||
case ColorComponent.Component1: |
|||
{ |
|||
var componentValue = MathUtilities.Clamp(sliderPercent * 360.0, 0.0, 360.0); |
|||
|
|||
hsvColor = new HsvColor( |
|||
hsvColor.A, |
|||
componentValue, |
|||
IsSaturationValueMaxForced ? 1.0 : hsvColor.S, |
|||
IsSaturationValueMaxForced ? 1.0 : hsvColor.V); |
|||
|
|||
break; |
|||
} |
|||
|
|||
case ColorComponent.Component2: |
|||
{ |
|||
var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); |
|||
|
|||
hsvColor = new HsvColor( |
|||
hsvColor.A, |
|||
hsvColor.H, |
|||
componentValue, |
|||
IsSaturationValueMaxForced ? 1.0 : hsvColor.V); |
|||
|
|||
break; |
|||
} |
|||
|
|||
case ColorComponent.Component3: |
|||
{ |
|||
var componentValue = MathUtilities.Clamp(sliderPercent * 1.0, 0.0, 1.0); |
|||
|
|||
hsvColor = new HsvColor( |
|||
hsvColor.A, |
|||
hsvColor.H, |
|||
IsSaturationValueMaxForced ? 1.0 : hsvColor.S, |
|||
componentValue); |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
selectedRgbColor = hsvColor.ToRgb(); |
|||
} |
|||
else |
|||
{ |
|||
if (IsAlphaMaxForced && |
|||
component != ColorComponent.Alpha) |
|||
{ |
|||
rgbColor = new Color(255, rgbColor.R, rgbColor.G, rgbColor.B); |
|||
} |
|||
|
|||
byte componentValue = Convert.ToByte(MathUtilities.Clamp(sliderPercent * 255, 0, 255)); |
|||
|
|||
switch (component) |
|||
{ |
|||
case ColorComponent.Component1: |
|||
rgbColor = new Color(rgbColor.A, componentValue, rgbColor.G, rgbColor.B); |
|||
break; |
|||
case ColorComponent.Component2: |
|||
rgbColor = new Color(rgbColor.A, rgbColor.R, componentValue, rgbColor.B); |
|||
break; |
|||
case ColorComponent.Component3: |
|||
rgbColor = new Color(rgbColor.A, rgbColor.R, rgbColor.G, componentValue); |
|||
break; |
|||
} |
|||
|
|||
selectedRgbColor = rgbColor; |
|||
} |
|||
|
|||
//var converter = new ContrastBrushConverter();
|
|||
//this.Foreground = converter.Convert(selectedRgbColor, typeof(Brush), this.DefaultForeground, null) as Brush;
|
|||
|
|||
return; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Generates a new background image for the color slider and applies it.
|
|||
/// </summary>
|
|||
private async void UpdateBackground(HsvColor color) |
|||
{ |
|||
// Updates may be requested when sliders are not in the visual tree.
|
|||
// For first-time load this is handled by the Loaded event.
|
|||
// However, after that problems may arise, consider the following case:
|
|||
//
|
|||
// (1) Backgrounds are drawn normally the first time on Loaded.
|
|||
// Actual height/width are available.
|
|||
// (2) The palette tab is selected which has no sliders
|
|||
// (3) The picker flyout is closed
|
|||
// (4) Externally the color is changed
|
|||
// The color change will trigger slider background updates but
|
|||
// with the flyout closed, actual height/width are zero.
|
|||
// No zero size bitmap can be generated.
|
|||
// (5) The picker flyout is re-opened by the user and the default
|
|||
// last-opened tab will be viewed: palette.
|
|||
// No loaded events will be fired for sliders. The color change
|
|||
// event was already handled in (4). The sliders will never
|
|||
// be updated.
|
|||
//
|
|||
// In this case the sliders become out of sync with the Color because there is no way
|
|||
// to tell when they actually come into view. To work around this, force a re-render of
|
|||
// the background with the last size of the slider. This last size will be when it was
|
|||
// last loaded or updated.
|
|||
//
|
|||
// In the future additional consideration may be required for SizeChanged of the control.
|
|||
// This work-around will also cause issues if display scaling changes in the special
|
|||
// case where cached sizes are required.
|
|||
|
|||
var width = Convert.ToInt32(Bounds.Width); |
|||
var height = Convert.ToInt32(Bounds.Height); |
|||
|
|||
if (width == 0 || height == 0) |
|||
{ |
|||
// Attempt to use the last size if it was available
|
|||
if (cachedSize.IsDefault == false) |
|||
{ |
|||
width = Convert.ToInt32(cachedSize.Width); |
|||
height = Convert.ToInt32(cachedSize.Height); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
cachedSize = new Size(width, height); |
|||
} |
|||
|
|||
var bitmap = await ColorHelpers.CreateComponentBitmapAsync( |
|||
width, |
|||
height, |
|||
Orientation, |
|||
ColorModel, |
|||
ColorComponent, |
|||
color, |
|||
IsAlphaMaxForced, |
|||
IsSaturationValueMaxForced); |
|||
|
|||
if (bitmap != null) |
|||
{ |
|||
Background = ColorHelpers.BitmapToBrushAsync(bitmap, width, height); |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
|||
{ |
|||
bool update = false; |
|||
|
|||
if (change.Property == ColorProperty) |
|||
{ |
|||
// Sync with HSV (which is primary)
|
|||
HsvColor = Color.ToHsv(); |
|||
update = true; |
|||
} |
|||
else if (change.Property == HsvColorProperty) |
|||
{ |
|||
update = true; |
|||
} |
|||
else if (change.Property == BoundsProperty) |
|||
{ |
|||
update = true; |
|||
} |
|||
|
|||
if (update && IsAutoUpdatingEnabled) |
|||
{ |
|||
UpdateColors(); |
|||
} |
|||
|
|||
base.OnPropertyChanged(change); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,172 @@ |
|||
<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="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="SliderThumbStyle" |
|||
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="SliderThumbStyle" |
|||
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.SliderThumbStyle"> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource SliderThumbBackground}" /> |
|||
<Setter Property="BorderThickness" Value="3" /> |
|||
</Style> |
|||
|
|||
<!-- PointerOver State --> |
|||
<Style Selector="ColorSlider:pointerover /template/ Thumb.SliderThumbStyle"> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource SliderThumbBackgroundPointerOver}" /> |
|||
</Style> |
|||
|
|||
<!-- Pressed State --> |
|||
<Style Selector="ColorSlider:pressed /template/ Thumb.SliderThumbStyle"> |
|||
<Setter Property="BorderBrush" Value="{DynamicResource SliderThumbBackgroundPressed}" /> |
|||
</Style> |
|||
|
|||
</Styles> |
|||
Loading…
Reference in new issue