From 6c33fe192a3b844e3d54afee72f223a83049ed8a Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 24 Aug 2025 15:46:04 -0400 Subject: [PATCH] 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 --- samples/ControlCatalog/Pages/BorderPage.xaml | 12 ++-- src/Avalonia.Base/Media/Brush.cs | 30 +++++----- src/Avalonia.Base/Media/KnownColors.cs | 56 ++++++++++++++++++- .../Media/BrushTests.cs | 22 +++++++- 4 files changed, 96 insertions(+), 24 deletions(-) diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml index 4c2384d686..c811ddaa22 100644 --- a/samples/ControlCatalog/Pages/BorderPage.xaml +++ b/samples/ControlCatalog/Pages/BorderPage.xaml @@ -20,26 +20,26 @@ Background And CenterBorder - Background And InnerBorder + Background And InnerBorderEdge Background And OuterBorderEdge Rounded Corners @@ -56,6 +56,6 @@ - + diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index 8c3491ed33..fb9dca26c9 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -1,9 +1,7 @@ using System; using System.ComponentModel; using Avalonia.Animation; -using Avalonia.Animation.Animators; using Avalonia.Media.Immutable; -using Avalonia.Reactive; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; @@ -40,8 +38,8 @@ namespace Avalonia.Media /// public double Opacity { - get { return GetValue(OpacityProperty); } - set { SetValue(OpacityProperty, value); } + get => GetValue(OpacityProperty); + set => SetValue(OpacityProperty, value); } /// @@ -49,8 +47,8 @@ namespace Avalonia.Media /// public ITransform? Transform { - get { return GetValue(TransformProperty); } - set { SetValue(TransformProperty, value); } + get => GetValue(TransformProperty); + set => SetValue(TransformProperty, value); } /// @@ -73,28 +71,30 @@ namespace Avalonia.Media if (s.Length > 0) { - if (s[0] == '#') - { - return new ImmutableSolidColorBrush(Color.Parse(s)); - } - + // Attempt to get a cached known brush first + // This is a performance optimization for known colors var brush = KnownColors.GetKnownBrush(s); if (brush != null) { return brush; } + + if (Color.TryParse(s, out Color color)) + { + return new ImmutableSolidColorBrush(color); + } } throw new FormatException($"Invalid brush string: '{s}'."); } - + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { if (change.Property == TransformProperty) _resource.ProcessPropertyChangeNotification(change); RegisterForSerialization(); - + base.OnPropertyChanged(change); } @@ -126,7 +126,7 @@ namespace Avalonia.Media if(_resource.Release(c)) OnUnreferencedFromCompositor(c); } - + protected virtual void OnUnreferencedFromCompositor(Compositor c) { if (Transform is ICompositionRenderResource resource) @@ -139,7 +139,7 @@ namespace Avalonia.Media { ServerCompositionSimpleBrush.SerializeAllChanges(writer, Opacity, TransformOrigin, Transform.GetServer(c)); } - + void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) => SerializeChanges(c, writer); } } diff --git a/src/Avalonia.Base/Media/KnownColors.cs b/src/Avalonia.Base/Media/KnownColors.cs index a0dea275bd..192eda9bc4 100644 --- a/src/Avalonia.Base/Media/KnownColors.cs +++ b/src/Avalonia.Base/Media/KnownColors.cs @@ -1,8 +1,7 @@ using System; -using System.Reflection; using System.Collections.Generic; -using Avalonia.SourceGenerator; using System.Diagnostics.CodeAnalysis; +using Avalonia.SourceGenerator; namespace Avalonia.Media { @@ -45,13 +44,47 @@ namespace Avalonia.Media } #if !BUILDTASK + + /// + /// Attempts to resolve a color name string to a solid color brush. + /// + /// + /// + /// Returns a cached immutable brush if the name matches one of the predefined + /// values. Repeated calls with the same color name will + /// return the same brush instance. + /// + /// + /// The lookup is case-sensitive and depends on the set of predefined known colors. + /// + /// + /// + /// The color name to look up (for example, "Red" or "CornflowerBlue"). + /// + /// + /// An corresponding to the specified name, + /// or null if the color name is not recognized. + /// public static ISolidColorBrush? GetKnownBrush(string s) { var color = GetKnownColor(s); return color != KnownColor.None ? color.ToBrush() : null; } + #endif + /// + /// Attempts to resolve a color name string to a value. + /// + /// + /// The lookup is case-sensitive and depends on the set of predefined known colors. + /// + /// + /// The color name to look up (for example, Red or CornflowerBlue). + /// + /// + /// A value if the name is recognized; otherwise . + /// public static KnownColor GetKnownColor(string s) { if (_knownColorNames.TryGetValue(s, out var color)) @@ -76,6 +109,20 @@ namespace Avalonia.Media } #if !BUILDTASK + + /// + /// Converts a value to an immutable solid color brush. + /// + /// + /// This method maintains an internal cache of brushes to avoid unnecessary allocations. + /// If the same is requested multiple times, the same immutable brush + /// instance is returned. + /// + /// The to convert. + /// + /// An instance representing the specified color. + /// Brushes created from known colors are cached and reused for efficiency. + /// public static IImmutableSolidColorBrush ToBrush(this KnownColor color) { lock (_knownBrushes) @@ -89,9 +136,14 @@ namespace Avalonia.Media return brush; } } + #endif + } + /// + /// Defines all known colors by name along with their 32-bit ARGB value. + /// internal enum KnownColor : uint { None, diff --git a/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs b/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs index 6b103ca684..c9c5b76b27 100644 --- a/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs @@ -1,6 +1,5 @@ using System; using Avalonia.Media; -using Avalonia.Rendering.Composition.Drawing; using Xunit; namespace Avalonia.Base.UnitTests.Media @@ -79,6 +78,27 @@ namespace Avalonia.Base.UnitTests.Media Assert.Throws(() => 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(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] public void Changing_Opacity_Raises_Invalidated() {