5 changed files with 410 additions and 0 deletions
@ -0,0 +1,112 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Data.Converters; |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <summary>
|
|||
/// Creates an accent color for a given base color value and step parameter.
|
|||
/// </summary>
|
|||
public class AccentColorConverter : IValueConverter |
|||
{ |
|||
/// <summary>
|
|||
/// The amount to change the Value channel 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; |
|||
|
|||
// Get the current color in HSV
|
|||
if (value is Color valueColor) |
|||
{ |
|||
rgbColor = valueColor; |
|||
} |
|||
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,69 @@ |
|||
using Avalonia.Media; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
/// <inheritdoc/>
|
|||
public partial class ColorPreviewer |
|||
{ |
|||
/// <summary>
|
|||
/// Defines the <see cref="Color"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<Color> ColorProperty = |
|||
AvaloniaProperty.Register<ColorSpectrum, Color>( |
|||
nameof(Color), |
|||
Colors.White); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the currently previewed color in the RGB color model.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// For control authors use <see cref="HsvColor"/> instead to avoid loss
|
|||
/// of precision and color drifting.
|
|||
/// </remarks>
|
|||
public Color Color |
|||
{ |
|||
get => GetValue(ColorProperty); |
|||
set => SetValue(ColorProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines the <see cref="HsvColor"/> property.
|
|||
/// </summary>
|
|||
public static readonly StyledProperty<HsvColor> HsvColorProperty = |
|||
AvaloniaProperty.Register<ColorPreviewer, HsvColor>( |
|||
nameof(HsvColor), |
|||
Colors.Transparent.ToHsv()); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the currently previewed color in the HSV color model.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This should be used in all cases instead of the <see cref="Color"/> property.
|
|||
/// Internally, the <see cref="ColorPreviewer"/> uses the HSV color model and using
|
|||
/// this property will avoid loss of precision and color drifting.
|
|||
/// </remarks>
|
|||
public HsvColor HsvColor |
|||
{ |
|||
get => GetValue(HsvColorProperty); |
|||
set => SetValue(HsvColorProperty, value); |
|||
} |
|||
|
|||
/// <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 a value indicating whether accent colors are shown along
|
|||
/// with the preview color.
|
|||
/// </summary>
|
|||
public bool ShowAccentColors |
|||
{ |
|||
get => (bool)this.GetValue(ShowAccentColorsProperty); |
|||
set => SetValue(ShowAccentColorsProperty, value); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,138 @@ |
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Controls.Metadata; |
|||
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 happens 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; |
|||
} |
|||
|
|||
return; |
|||
} |
|||
|
|||
/// <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) |
|||
{ |
|||
// Always keep the two color properties in sync
|
|||
if (change.Property == ColorProperty) |
|||
{ |
|||
HsvColor = Color.ToHsv(); |
|||
} |
|||
else if (change.Property == HsvColorProperty) |
|||
{ |
|||
Color = HsvColor.ToRgb(); |
|||
} |
|||
|
|||
base.OnPropertyChanged(change); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Called before the <see cref="ColorChanged"/> event occurs.
|
|||
/// </summary>
|
|||
/// <param name="newColor">The newly selected color.</param>
|
|||
protected virtual void OnColorChanged(HsvColor newColor) |
|||
{ |
|||
var oldColor = HsvColor; |
|||
HsvColor = newColor; |
|||
|
|||
ColorChanged?.Invoke(this, new ColorChangedEventArgs(oldColor.ToRgb(), newColor.ToRgb())); |
|||
|
|||
return; |
|||
} |
|||
|
|||
/// <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); |
|||
OnColorChanged(newHsvColor); |
|||
|
|||
return; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:primitives="using:Avalonia.Controls.Primitives" |
|||
x:CompileBindings="True"> |
|||
|
|||
<Styles.Resources> |
|||
<primitives:AccentColorConverter x:Key="AccentColor" /> |
|||
</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" |
|||
Margin="0,0,-10,0" |
|||
VerticalAlignment="Center" |
|||
IsVisible="{TemplateBinding ShowAccentColors}"> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition Width="*" /> |
|||
<ColumnDefinition Width="*" /> |
|||
</Grid.ColumnDefinitions> |
|||
<Border Grid.Column="0" |
|||
Grid.ColumnSpan="2" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" |
|||
Background="{StaticResource CheckeredBackgroundBrush}" /> |
|||
<Border x:Name="AccentDec2Border" |
|||
Grid.Column="0" |
|||
CornerRadius="4,0,0,4" |
|||
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" |
|||
Margin="-10,0,0,0" |
|||
VerticalAlignment="Center" |
|||
IsVisible="{TemplateBinding ShowAccentColors}"> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition Width="*" /> |
|||
<ColumnDefinition Width="*" /> |
|||
</Grid.ColumnDefinitions> |
|||
<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="0,4,4,0" |
|||
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"> |
|||
<Grid> |
|||
<Border Background="{DynamicResource FlyoutPresenterBackground}" |
|||
CornerRadius="{TemplateBinding CornerRadius}" /> |
|||
<Border Background="{StaticResource CheckeredBackgroundBrush}" |
|||
CornerRadius="{TemplateBinding CornerRadius}" /> |
|||
<Border x:Name="PreviewBorder" |
|||
CornerRadius="{TemplateBinding CornerRadius}" |
|||
Background="{TemplateBinding HsvColor, Converter={StaticResource AccentColor}, ConverterParameter='0'}" |
|||
HorizontalAlignment="Stretch" |
|||
VerticalAlignment="Stretch" /> |
|||
</Grid> |
|||
</Border> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
</Styles> |
|||
Loading…
Reference in new issue