Browse Source

Merge pull request #5092 from AvaloniaUI/fixes/4900-xaml-dashstyle

Allow specifying DashStyle.Dashes in XAML
Benedikt Stebner 5 years ago
parent
commit
ee114264d9

+ 4 - 38
src/Avalonia.Visuals/ApiCompatBaseline.txt

@@ -1,39 +1,5 @@
 Compat issues with assembly Avalonia.Visuals:
-MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.Geometry.GetRenderBounds(Avalonia.Media.Pen)' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public System.Boolean Avalonia.Media.Geometry.StrokeContains(Avalonia.Media.Pen, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.GlyphRun.Bounds.get()' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public Avalonia.StyledProperty<Avalonia.Point> Avalonia.StyledProperty<Avalonia.Point> Avalonia.Media.GlyphRunDrawing.BaselineOriginProperty' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public Avalonia.Point Avalonia.Media.GlyphRunDrawing.BaselineOrigin.get()' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public void Avalonia.Media.GlyphRunDrawing.BaselineOrigin.set(Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract.
-TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract.
-CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract.
-CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract.
-TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract.
-CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size' is abstract in the implementation but is missing in the contract.
-MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.DrawableTextRun.Bounds.get()' does not exist in the implementation but it does exist in the contract.
-CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract.
-MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size.get()' is abstract in the implementation but is missing in the contract.
-MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.ShapedTextCharacters.Bounds.get()' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract.
-CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract.
-MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract.
-CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IDrawingContextLayerImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the implementation but not in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the contract but not in the implementation.
-MembersMustExist : Member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' is present in the contract but not in the implementation.
-MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation.
-MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract.
-InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract.
-MembersMustExist : Member 'public Avalonia.Utilities.IRef<Avalonia.Platform.IRenderTargetBitmapImpl> Avalonia.Rendering.RenderLayer.Bitmap.get()' does not exist in the implementation but it does exist in the contract.
-Total Issues: 37
+MembersMustExist : Member 'public Avalonia.StyledProperty<System.Collections.Generic.IReadOnlyList<System.Double>> Avalonia.StyledProperty<System.Collections.Generic.IReadOnlyList<System.Double>> Avalonia.Media.DashStyle.DashesProperty' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyList<System.Double> Avalonia.Media.DashStyle.Dashes.get()' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public void Avalonia.Media.DashStyle.Dashes.set(System.Collections.Generic.IReadOnlyList<System.Double>)' does not exist in the implementation but it does exist in the contract.
+Total Issues: 3

+ 51 - 24
src/Avalonia.Visuals/Media/DashStyle.cs

@@ -1,11 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Avalonia.Animation;
+using Avalonia.Collections;
+using Avalonia.Media.Immutable;
+
+#nullable enable
+
 namespace Avalonia.Media
 {
-    using System;
-    using System.Collections.Generic;
-    using System.Linq;
-    using Avalonia.Animation;
-    using Avalonia.Media.Immutable;
-
     /// <summary>
     /// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
     /// </summary>
@@ -14,8 +17,8 @@ namespace Avalonia.Media
         /// <summary>
         /// Defines the <see cref="Dashes"/> property.
         /// </summary>
-        public static readonly StyledProperty<IReadOnlyList<double>> DashesProperty =
-            AvaloniaProperty.Register<DashStyle, IReadOnlyList<double>>(nameof(Dashes));
+        public static readonly StyledProperty<AvaloniaList<double>> DashesProperty =
+            AvaloniaProperty.Register<DashStyle, AvaloniaList<double>>(nameof(Dashes));
 
         /// <summary>
         /// Defines the <see cref="Offset"/> property.
@@ -23,10 +26,10 @@ namespace Avalonia.Media
         public static readonly StyledProperty<double> OffsetProperty =
             AvaloniaProperty.Register<DashStyle, double>(nameof(Offset));
 
-        private static ImmutableDashStyle s_dash;
-        private static ImmutableDashStyle s_dot;
-        private static ImmutableDashStyle s_dashDot;
-        private static ImmutableDashStyle s_dashDotDot;
+        private static ImmutableDashStyle? s_dash;
+        private static ImmutableDashStyle? s_dot;
+        private static ImmutableDashStyle? s_dashDot;
+        private static ImmutableDashStyle? s_dashDotDot;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DashStyle"/> class.
@@ -41,9 +44,9 @@ namespace Avalonia.Media
         /// </summary>
         /// <param name="dashes">The dashes collection.</param>
         /// <param name="offset">The dash sequence offset.</param>
-        public DashStyle(IEnumerable<double> dashes, double offset)
+        public DashStyle(IEnumerable<double>? dashes, double offset)
         {
-            Dashes = (IReadOnlyList<double>)dashes?.ToList() ?? Array.Empty<double>();
+            Dashes = (dashes as AvaloniaList<double>) ?? new AvaloniaList<double>(dashes ?? Array.Empty<double>());
             Offset = offset;
         }
 
@@ -61,31 +64,27 @@ namespace Avalonia.Media
         /// <summary>
         /// Represents a dashed <see cref="DashStyle"/>.
         /// </summary>
-        public static IDashStyle Dash =>
-            s_dash ?? (s_dash = new ImmutableDashStyle(new double[] { 2, 2 }, 1));
+        public static IDashStyle Dash => s_dash ??= new ImmutableDashStyle(new double[] { 2, 2 }, 1);
 
         /// <summary>
         /// Represents a dotted <see cref="DashStyle"/>.
         /// </summary>
-        public static IDashStyle Dot =>
-            s_dot ?? (s_dot = new ImmutableDashStyle(new double[] { 0, 2 }, 0));
+        public static IDashStyle Dot => s_dot ??= new ImmutableDashStyle(new double[] { 0, 2 }, 0);
 
         /// <summary>
         /// Represents a dashed dotted <see cref="DashStyle"/>.
         /// </summary>
-        public static IDashStyle DashDot =>
-            s_dashDot ?? (s_dashDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1));
+        public static IDashStyle DashDot => s_dashDot ??= new ImmutableDashStyle(new double[] { 2, 2, 0, 2 }, 1);
 
         /// <summary>
         /// Represents a dashed double dotted <see cref="DashStyle"/>.
         /// </summary>
-        public static IDashStyle DashDotDot =>
-            s_dashDotDot ?? (s_dashDotDot = new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1));
+        public static IDashStyle DashDotDot => s_dashDotDot ??= new ImmutableDashStyle(new double[] { 2, 2, 0, 2, 0, 2 }, 1);
 
         /// <summary>
         /// Gets or sets the length of alternating dashes and gaps.
         /// </summary>
-        public IReadOnlyList<double> Dashes
+        public AvaloniaList<double> Dashes
         {
             get => GetValue(DashesProperty);
             set => SetValue(DashesProperty, value);
@@ -100,15 +99,43 @@ namespace Avalonia.Media
             set => SetValue(OffsetProperty, value);
         }
 
+        IReadOnlyList<double> IDashStyle.Dashes => Dashes;
+
         /// <summary>
         /// Raised when the dash style changes.
         /// </summary>
-        public event EventHandler Invalidated;
+        public event EventHandler? Invalidated;
 
         /// <summary>
         /// Returns an immutable clone of the <see cref="DashStyle"/>.
         /// </summary>
         /// <returns></returns>
         public ImmutableDashStyle ToImmutable() => new ImmutableDashStyle(Dashes, Offset);
+
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == DashesProperty)
+            {
+                var oldValue = change.OldValue.GetValueOrDefault<AvaloniaList<double>>();
+                var newValue = change.NewValue.GetValueOrDefault<AvaloniaList<double>>();
+
+                if (oldValue is object)
+                {
+                    oldValue.CollectionChanged -= DashesChanged;
+                }
+
+                if (newValue is object)
+                {
+                    newValue.CollectionChanged += DashesChanged;
+                }
+            }
+        }
+
+        private void DashesChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            Invalidated?.Invoke(this, e);
+        }
     }
 }

+ 24 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ShapeTests.cs

@@ -0,0 +1,24 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Xaml
+{
+    public class ShapeTests : XamlTestBase
+    {
+        [Fact]
+        public void Can_Specify_DashStyle_In_XAML()
+        {
+            var xaml = @"
+<Pen xmlns='https://github.com/avaloniaui'>
+    <Pen.DashStyle>
+	    <DashStyle Offset='0' Dashes='1,3'/>
+    </Pen.DashStyle>
+</Pen>";
+
+            var target = AvaloniaRuntimeXamlLoader.Parse<Pen>(xaml);
+
+            Assert.NotNull(target);
+        }
+    }
+}

+ 16 - 2
tests/Avalonia.Visuals.UnitTests/Media/PenTests.cs

@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using Avalonia.Collections;
+using Avalonia.Media;
 using Avalonia.Media.Immutable;
 using Xunit;
 
@@ -39,7 +40,20 @@ namespace Avalonia.Visuals.UnitTests.Media
             var raised = false;
 
             target.Invalidated += (s, e) => raised = true;
-            dashes.Dashes = new[] { 0.1, 0.2 };
+            dashes.Dashes = new AvaloniaList<double> { 0.1, 0.2 };
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void Adding_DashStyle_Dashes_Raises_Invalidated()
+        {
+            var dashes = new DashStyle();
+            var target = new Pen { DashStyle = dashes };
+            var raised = false;
+
+            target.Invalidated += (s, e) => raised = true;
+            dashes.Dashes.Add(0.3);
 
             Assert.True(raised);
         }