Browse Source

Update Brush.Parse() to Handle all Color Formats (#19526)

* Add unit test for all color format Brush parsing

* Support parsing all known color formats in Brush.Parse()

* Update property syntax

* Remove unnecessary usings

* Use an actual hex color

* Fix new brush test

* Switch order of brush parsing so known color optimizations are used

* Add more documentation comments to KnownColors

* Fix displayed text in BorderPage

InnerBorderEdge not InnerBorder
pull/19540/head
robloo 6 months ago
committed by GitHub
parent
commit
6c33fe192a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      samples/ControlCatalog/Pages/BorderPage.xaml
  2. 30
      src/Avalonia.Base/Media/Brush.cs
  3. 56
      src/Avalonia.Base/Media/KnownColors.cs
  4. 22
      tests/Avalonia.Base.UnitTests/Media/BrushTests.cs

12
samples/ControlCatalog/Pages/BorderPage.xaml

@ -20,26 +20,26 @@
<Border Background="{DynamicResource SystemAccentColorDark1}" <Border Background="{DynamicResource SystemAccentColorDark1}"
BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}" BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}"
BackgroundSizing="CenterBorder" BackgroundSizing="CenterBorder"
BorderThickness="8" BorderThickness="8"
Padding="12"> Padding="12">
<TextBlock>Background And CenterBorder</TextBlock> <TextBlock>Background And CenterBorder</TextBlock>
</Border> </Border>
<Border Background="{DynamicResource SystemAccentColorDark1}" <Border Background="{DynamicResource SystemAccentColorDark1}"
BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}" BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}"
BackgroundSizing="InnerBorderEdge" BackgroundSizing="InnerBorderEdge"
BorderThickness="8" BorderThickness="8"
Padding="12"> Padding="12">
<TextBlock>Background And InnerBorder</TextBlock> <TextBlock>Background And InnerBorderEdge</TextBlock>
</Border> </Border>
<Border Background="{DynamicResource SystemAccentColorDark1}" <Border Background="{DynamicResource SystemAccentColorDark1}"
BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}" BorderBrush="{DynamicResource SemiTransparentSystemAccentBrush}"
BackgroundSizing="OuterBorderEdge" BackgroundSizing="OuterBorderEdge"
BorderThickness="8" BorderThickness="8"
Padding="12"> Padding="12">
<TextBlock>Background And OuterBorderEdge</TextBlock> <TextBlock>Background And OuterBorderEdge</TextBlock>
</Border> </Border>
<Border BorderBrush="{DynamicResource SystemAccentColor}" <Border BorderBrush="{DynamicResource SystemAccentColor}"
BorderThickness="4" BorderThickness="4"
CornerRadius="8" CornerRadius="8"
Padding="16"> Padding="16">
<TextBlock>Rounded Corners</TextBlock> <TextBlock>Rounded Corners</TextBlock>
@ -56,6 +56,6 @@
<Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill" /> <Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill" />
</Border> </Border>
<TextBlock Text="Border with Clipping" HorizontalAlignment="Center" /> <TextBlock Text="Border with Clipping" HorizontalAlignment="Center" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

30
src/Avalonia.Base/Media/Brush.cs

@ -1,9 +1,7 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Animation.Animators;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
@ -40,8 +38,8 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public double Opacity public double Opacity
{ {
get { return GetValue(OpacityProperty); } get => GetValue(OpacityProperty);
set { SetValue(OpacityProperty, value); } set => SetValue(OpacityProperty, value);
} }
/// <summary> /// <summary>
@ -49,8 +47,8 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public ITransform? Transform public ITransform? Transform
{ {
get { return GetValue(TransformProperty); } get => GetValue(TransformProperty);
set { SetValue(TransformProperty, value); } set => SetValue(TransformProperty, value);
} }
/// <summary> /// <summary>
@ -73,28 +71,30 @@ namespace Avalonia.Media
if (s.Length > 0) if (s.Length > 0)
{ {
if (s[0] == '#') // Attempt to get a cached known brush first
{ // This is a performance optimization for known colors
return new ImmutableSolidColorBrush(Color.Parse(s));
}
var brush = KnownColors.GetKnownBrush(s); var brush = KnownColors.GetKnownBrush(s);
if (brush != null) if (brush != null)
{ {
return brush; return brush;
} }
if (Color.TryParse(s, out Color color))
{
return new ImmutableSolidColorBrush(color);
}
} }
throw new FormatException($"Invalid brush string: '{s}'."); throw new FormatException($"Invalid brush string: '{s}'.");
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (change.Property == TransformProperty) if (change.Property == TransformProperty)
_resource.ProcessPropertyChangeNotification(change); _resource.ProcessPropertyChangeNotification(change);
RegisterForSerialization(); RegisterForSerialization();
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
} }
@ -126,7 +126,7 @@ namespace Avalonia.Media
if(_resource.Release(c)) if(_resource.Release(c))
OnUnreferencedFromCompositor(c); OnUnreferencedFromCompositor(c);
} }
protected virtual void OnUnreferencedFromCompositor(Compositor c) protected virtual void OnUnreferencedFromCompositor(Compositor c)
{ {
if (Transform is ICompositionRenderResource<ITransform> resource) if (Transform is ICompositionRenderResource<ITransform> resource)
@ -139,7 +139,7 @@ namespace Avalonia.Media
{ {
ServerCompositionSimpleBrush.SerializeAllChanges(writer, Opacity, TransformOrigin, Transform.GetServer(c)); ServerCompositionSimpleBrush.SerializeAllChanges(writer, Opacity, TransformOrigin, Transform.GetServer(c));
} }
void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) => SerializeChanges(c, writer); void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) => SerializeChanges(c, writer);
} }
} }

56
src/Avalonia.Base/Media/KnownColors.cs

@ -1,8 +1,7 @@
using System; using System;
using System.Reflection;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.SourceGenerator;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia.SourceGenerator;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -45,13 +44,47 @@ namespace Avalonia.Media
} }
#if !BUILDTASK #if !BUILDTASK
/// <summary>
/// Attempts to resolve a color name string to a solid color brush.
/// </summary>
/// <remarks>
/// <para>
/// Returns a cached immutable brush if the name matches one of the predefined
/// <see cref="KnownColor"/> values. Repeated calls with the same color name will
/// return the same brush instance.
/// </para>
/// <para>
/// The lookup is case-sensitive and depends on the set of predefined known colors.
/// </para>
/// </remarks>
/// <param name="s">
/// The color name to look up (for example, <c>"Red"</c> or <c>"CornflowerBlue"</c>).
/// </param>
/// <returns>
/// An <see cref="ISolidColorBrush"/> corresponding to the specified name,
/// or <c>null</c> if the color name is not recognized.
/// </returns>
public static ISolidColorBrush? GetKnownBrush(string s) public static ISolidColorBrush? GetKnownBrush(string s)
{ {
var color = GetKnownColor(s); var color = GetKnownColor(s);
return color != KnownColor.None ? color.ToBrush() : null; return color != KnownColor.None ? color.ToBrush() : null;
} }
#endif #endif
/// <summary>
/// Attempts to resolve a color name string to a <see cref="KnownColor"/> value.
/// </summary>
/// <remarks>
/// The lookup is case-sensitive and depends on the set of predefined known colors.
/// </remarks>
/// <param name="s">
/// The color name to look up (for example, <c>Red</c> or <c>CornflowerBlue</c>).
/// </param>
/// <returns>
/// A <see cref="KnownColor"/> value if the name is recognized; otherwise <see cref="KnownColor.None"/>.
/// </returns>
public static KnownColor GetKnownColor(string s) public static KnownColor GetKnownColor(string s)
{ {
if (_knownColorNames.TryGetValue(s, out var color)) if (_knownColorNames.TryGetValue(s, out var color))
@ -76,6 +109,20 @@ namespace Avalonia.Media
} }
#if !BUILDTASK #if !BUILDTASK
/// <summary>
/// Converts a <see cref="KnownColor"/> value to an immutable solid color brush.
/// </summary>
/// <remarks>
/// This method maintains an internal cache of brushes to avoid unnecessary allocations.
/// If the same <paramref name="color"/> is requested multiple times, the same immutable brush
/// instance is returned.
/// </remarks>
/// <param name="color">The <see cref="KnownColor"/> to convert.</param>
/// <returns>
/// An <see cref="IImmutableSolidColorBrush"/> instance representing the specified color.
/// Brushes created from known colors are cached and reused for efficiency.
/// </returns>
public static IImmutableSolidColorBrush ToBrush(this KnownColor color) public static IImmutableSolidColorBrush ToBrush(this KnownColor color)
{ {
lock (_knownBrushes) lock (_knownBrushes)
@ -89,9 +136,14 @@ namespace Avalonia.Media
return brush; return brush;
} }
} }
#endif #endif
} }
/// <summary>
/// Defines all known colors by name along with their 32-bit ARGB value.
/// </summary>
internal enum KnownColor : uint internal enum KnownColor : uint
{ {
None, None,

22
tests/Avalonia.Base.UnitTests/Media/BrushTests.cs

@ -1,6 +1,5 @@
using System; using System;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering.Composition.Drawing;
using Xunit; using Xunit;
namespace Avalonia.Base.UnitTests.Media namespace Avalonia.Base.UnitTests.Media
@ -79,6 +78,27 @@ namespace Avalonia.Base.UnitTests.Media
Assert.Throws<FormatException>(() => Brush.Parse("#ff808g80")); Assert.Throws<FormatException>(() => Brush.Parse("#ff808g80"));
} }
[Theory]
[InlineData("rgb(255, 128, 64)")]
[InlineData("rgba(255, 128, 64, 0.5)")]
[InlineData("hsl(120, 100%, 50%)")]
[InlineData("hsla(120, 100%, 50%, 0.5)")]
[InlineData("hsv(300, 100%, 25%)")]
[InlineData("hsva(300, 100%, 25%, 0.75)")]
[InlineData("#40ff8844")]
[InlineData("Green")]
public void Parse_Parses_All_Color_Format_Brushes(string input)
{
var brush = Brush.Parse(input);
Assert.IsAssignableFrom<ISolidColorBrush>(brush);
// The ColorTests already validate all color formats are parsed properly
// Since Brush.Parse() forwards to Color.Parse() we don't need to repeat this
// We can simply check if the parsed Brush's color matches what Color.Parse provides
var expected = Color.Parse(input);
Assert.Equal(expected, (brush as ISolidColorBrush)?.Color);
}
[Fact] [Fact]
public void Changing_Opacity_Raises_Invalidated() public void Changing_Opacity_Raises_Invalidated()
{ {

Loading…
Cancel
Save