Browse Source

Convert Color resources to brushes.

Make `StaticResource` and `DynamicResource` automatically convert `Color`s to `ImmutableSolidColorBrush`es and add a `ColorToBrushConverter` for use by bindings.

To do this I made `GetResourceObservable` accept an optional conversion function.
Steven Kirk 5 years ago
parent
commit
f46c62801e

+ 25 - 10
src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Data.Converters;
 using Avalonia.LogicalTree;
 using Avalonia.Reactive;
 
@@ -58,31 +59,39 @@ namespace Avalonia.Controls
             return false;
         }
 
-        public static IObservable<object?> GetResourceObservable(this IStyledElement control, object key)
+        public static IObservable<object?> GetResourceObservable(
+            this IStyledElement control,
+            object key,
+            Func<object?, object?>? converter = null)
         {
             control = control ?? throw new ArgumentNullException(nameof(control));
             key = key ?? throw new ArgumentNullException(nameof(key));
 
-            return new ResourceObservable(control, key);
+            return new ResourceObservable(control, key, converter);
         }
 
-        public static IObservable<object?> GetResourceObservable(this IResourceProvider resourceProvider, object key)
+        public static IObservable<object?> GetResourceObservable(
+            this IResourceProvider resourceProvider,
+            object key,
+            Func<object?, object?>? converter = null)
         {
             resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider));
             key = key ?? throw new ArgumentNullException(nameof(key));
 
-            return new FloatingResourceObservable(resourceProvider, key);
+            return new FloatingResourceObservable(resourceProvider, key, converter);
         }
 
         private class ResourceObservable : LightweightObservableBase<object?>
         {
             private readonly IStyledElement _target;
             private readonly object _key;
+            private readonly Func<object?, object?>? _converter;
 
-            public ResourceObservable(IStyledElement target, object key)
+            public ResourceObservable(IStyledElement target, object key, Func<object?, object?>? converter)
             {
                 _target = target;
                 _key = key;
+                _converter = converter;
             }
 
             protected override void Initialize()
@@ -97,25 +106,29 @@ namespace Avalonia.Controls
 
             protected override void Subscribed(IObserver<object?> observer, bool first)
             {
-                observer.OnNext(_target.FindResource(_key));
+                observer.OnNext(Convert(_target.FindResource(_key)));
             }
 
             private void ResourcesChanged(object sender, ResourcesChangedEventArgs e)
             {
-                PublishNext(_target.FindResource(_key));
+                PublishNext(Convert(_target.FindResource(_key)));
             }
+
+            private object? Convert(object? value) => _converter?.Invoke(value) ?? value;
         }
 
         private class FloatingResourceObservable : LightweightObservableBase<object?>
         {
             private readonly IResourceProvider _target;
             private readonly object _key;
+            private readonly Func<object?, object?>? _converter;
             private IResourceHost? _owner;
 
-            public FloatingResourceObservable(IResourceProvider target, object key)
+            public FloatingResourceObservable(IResourceProvider target, object key, Func<object?, object?>? converter)
             {
                 _target = target;
                 _key = key;
+                _converter = converter;
             }
 
             protected override void Initialize()
@@ -134,13 +147,13 @@ namespace Avalonia.Controls
             {
                 if (_target.Owner is object)
                 {
-                    observer.OnNext(_target.Owner?.FindResource(_key));
+                    observer.OnNext(Convert(_target.Owner?.FindResource(_key)));
                 }
             }
 
             private void PublishNext()
             {
-                PublishNext(_target.Owner?.FindResource(_key));
+                PublishNext(Convert(_target.Owner?.FindResource(_key)));
             }
 
             private void OwnerChanged(object sender, EventArgs e)
@@ -164,6 +177,8 @@ namespace Avalonia.Controls
             {
                 PublishNext();
             }
+
+            private object? Convert(object? value) => _converter?.Invoke(value) ?? value;
         }
     }
 }

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -11,6 +11,7 @@
     <ItemGroup>
         <Compile Include="AvaloniaXamlLoader.cs" />
         <Compile Include="Converters\AvaloniaUriTypeConverter.cs" />
+        <Compile Include="Converters\ColorToBrushConverter.cs" />
         <Compile Include="Converters\FontFamilyTypeConverter.cs" />
         <Compile Include="Converters\TimeSpanTypeConverter.cs" />
         <Compile Include="Extensions.cs" />

+ 88 - 0
src/Markup/Avalonia.Markup.Xaml/Converters/ColorToBrushConverter.cs

@@ -0,0 +1,88 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+    /// <summary>
+    /// Converts a <see cref="Color"/> to an <see cref="IBrush"/>.
+    /// </summary>
+    public class ColorToBrushConverter : IValueConverter
+    {
+        /// <summary>
+        /// Converts a <see cref="Color"/> to an <see cref="IBrush"/> if the arguments are of the
+        /// correct type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <param name="targetType">The target type.</param>
+        /// <param name="parameter">Not used.</param>
+        /// <param name="culture">Not used.</param>
+        /// <returns>
+        /// If <paramref name="value"/> is a <see cref="Color"/> and <paramref name="targetType"/>
+        /// is <see cref="IBrush"/> then converts the color to a solid color brush.
+        /// </returns>
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return Convert(value, targetType);
+        }
+
+        /// <summary>
+        /// Converts an <see cref="ISolidColorBrush"/> to a <see cref="Color"/> if the arguments are of the
+        /// correct type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <param name="targetType">The target type.</param>
+        /// <param name="parameter">Not used.</param>
+        /// <param name="culture">Not used.</param>
+        /// <returns>
+        /// If <paramref name="value"/> is an <see cref="ISolidColorBrush"/> and <paramref name="targetType"/>
+        /// is <see cref="Color"/> then converts the solid color brush to a color.
+        /// </returns>
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return ConvertBack(value, targetType);
+        }
+
+        /// <summary>
+        /// Converts a <see cref="Color"/> to an <see cref="IBrush"/> if the arguments are of the
+        /// correct type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <param name="targetType">The target type.</param>
+        /// <returns>
+        /// If <paramref name="value"/> is a <see cref="Color"/> and <paramref name="targetType"/>
+        /// is <see cref="IBrush"/> then converts the color to a solid color brush.
+        /// </returns>
+        public static object Convert(object value, Type targetType)
+        {
+            if (targetType == typeof(IBrush) && value is Color c)
+            {
+                return new ImmutableSolidColorBrush(c);
+            }
+
+            return value;
+        }
+
+        /// <summary>
+        /// Converts an <see cref="ISolidColorBrush"/> to a <see cref="Color"/> if the arguments are of the
+        /// correct type.
+        /// </summary>
+        /// <param name="value">The value.</param>
+        /// <param name="targetType">The target type.</param>
+        /// <returns>
+        /// If <paramref name="value"/> is an <see cref="ISolidColorBrush"/> and <paramref name="targetType"/>
+        /// is <see cref="Color"/> then converts the solid color brush to a color.
+        /// </returns>
+        public static object ConvertBack(object value, Type targetType)
+        {
+            if (targetType == typeof(Color) && value is ISolidColorBrush brush)
+            {
+                return brush.Color;
+            }
+
+            return value;
+        }
+    }
+}

+ 16 - 2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@@ -1,6 +1,8 @@
 using System;
 using Avalonia.Controls;
 using Avalonia.Data;
+using Avalonia.Markup.Xaml.Converters;
+using Avalonia.Media;
 
 #nullable enable
 
@@ -54,11 +56,23 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
 
             if (control != null)
             {
-                return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey));
+                var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty));
+                return InstancedBinding.OneWay(source);
             }
             else if (_resourceProvider is object)
             {
-                return InstancedBinding.OneWay(_resourceProvider.GetResourceObservable(ResourceKey));
+                var source = _resourceProvider.GetResourceObservable(ResourceKey, GetConverter(targetProperty));
+                return InstancedBinding.OneWay(source);
+            }
+
+            return null;
+        }
+
+        private Func<object?, object?>? GetConverter(AvaloniaProperty targetProperty)
+        {
+            if (targetProperty?.PropertyType == typeof(IBrush))
+            {
+                return x => ColorToBrushConverter.Convert(x, typeof(IBrush));
             }
 
             return null;

+ 14 - 9
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@@ -1,9 +1,9 @@
 using System;
 using System.Collections.Generic;
-using System.ComponentModel;
 using System.Reflection;
 using Avalonia.Controls;
 using Avalonia.Markup.Data;
+using Avalonia.Markup.Xaml.Converters;
 using Avalonia.Markup.Xaml.XamlIl.Runtime;
 
 namespace Avalonia.Markup.Xaml.MarkupExtensions
@@ -24,6 +24,14 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
         public object ProvideValue(IServiceProvider serviceProvider)
         {
             var stack = serviceProvider.GetService<IAvaloniaXamlIlParentStackProvider>();
+            var provideTarget = serviceProvider.GetService<IProvideValueTarget>();
+
+            var targetType = provideTarget.TargetProperty switch
+            {
+                AvaloniaProperty ap => ap.PropertyType,
+                PropertyInfo pi => pi.PropertyType,
+                _ => null,
+            };
 
             // Look upwards though the ambient context for IResourceHosts and IResourceProviders
             // which might be able to give us the resource.
@@ -33,30 +41,27 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
 
                 if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value))
                 {
-                    return value;
+                    return ColorToBrushConverter.Convert(value, targetType);
                 }
                 else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value))
                 {
-                    return value;
+                    return ColorToBrushConverter.Convert(value, targetType);
                 }
             }
 
-            // The resource still hasn't been found, so add a delayed one-time binding.
-            var provideTarget = serviceProvider.GetService<IProvideValueTarget>();
-
             if (provideTarget.TargetObject is IControl target &&
                 provideTarget.TargetProperty is PropertyInfo property)
             {
-                DelayedBinding.Add(target, property, GetValue);
+                DelayedBinding.Add(target, property, x => GetValue(x, targetType));
                 return AvaloniaProperty.UnsetValue;
             }
 
             throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found.");
         }
 
-        private object GetValue(IStyledElement control)
+        private object GetValue(IStyledElement control, Type targetType)
         {
-            return control.FindResource(ResourceKey);
+            return ColorToBrushConverter.Convert(control.FindResource(ResourceKey), targetType);
         }
     }
 }

+ 21 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@@ -797,6 +797,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             }
         }
 
+        [Fact]
+        public void Automatically_Converts_Color_To_SolidColorBrush()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <Color x:Key='color'>#ff506070</Color>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{DynamicResource color}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (ISolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
         private IDisposable StyledWindow(params (string, string)[] assets)
         {
             var services = TestServices.StyledWindow.With(

+ 21 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs

@@ -511,6 +511,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             Assert.Equal(0xff506070, brush.Color.ToUint32());
         }
 
+        [Fact]
+        public void Automatically_Converts_Color_To_SolidColorBrush()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <Color x:Key='color'>#ff506070</Color>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{StaticResource color}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (ISolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
         private IDisposable StyledWindow(params (string, string)[] assets)
         {
             var services = TestServices.StyledWindow.With(