From 71aba01b1002580696721c2f236dc2c904e9706c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:20:11 +0200 Subject: [PATCH 1/9] Implement FormattedText.BuildGeometry --- .../Pages/FormattedTextPage.axaml.cs | 4 + src/Avalonia.Base/GeometryCollection.cs | 37 -- src/Avalonia.Base/Media/DrawingCollection.cs | 18 + src/Avalonia.Base/Media/DrawingGroup.cs | 420 +++++++++++++++++- src/Avalonia.Base/Media/FormattedText.cs | 129 +++++- src/Avalonia.Base/Media/GeometryCollection.cs | 45 ++ src/Avalonia.Base/Media/GeometryDrawing.cs | 12 +- .../{ => Media}/GeometryGroup.cs | 48 +- src/Avalonia.Base/Media/GlyphRun.cs | 10 +- .../Platform/IPlatformRenderInterface.cs | 3 +- .../HeadlessPlatformRenderInterface.cs | 4 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 26 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 22 +- .../Media/GeometryGroupTests.cs | 18 + .../VisualTree/MockRenderInterface.cs | 2 +- .../NullRenderingPlatform.cs | 2 +- .../Media/GlyphRunTests.cs | 35 +- .../MockPlatformRenderInterface.cs | 4 +- ...ould_Render_GlyphRun_Geometry.expected.png | Bin 5326 -> 5369 bytes ...ould_Render_GlyphRun_Geometry.expected.png | Bin 4228 -> 4149 bytes 20 files changed, 728 insertions(+), 111 deletions(-) delete mode 100644 src/Avalonia.Base/GeometryCollection.cs create mode 100644 src/Avalonia.Base/Media/DrawingCollection.cs create mode 100644 src/Avalonia.Base/Media/GeometryCollection.cs rename src/Avalonia.Base/{ => Media}/GeometryGroup.cs (69%) diff --git a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs index 25e29c67a9..97a9320c95 100644 --- a/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs +++ b/samples/RenderDemo/Pages/FormattedTextPage.axaml.cs @@ -55,6 +55,10 @@ namespace RenderDemo.Pages formattedText.SetFontStyle(FontStyle.Italic, 28, 28); context.DrawText(formattedText, new Point(10, 0)); + + var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0)); + + context.DrawGeometry(gradient, null, geometry); } } } diff --git a/src/Avalonia.Base/GeometryCollection.cs b/src/Avalonia.Base/GeometryCollection.cs deleted file mode 100644 index 0bd02d5438..0000000000 --- a/src/Avalonia.Base/GeometryCollection.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Avalonia.Animation; - -#nullable enable - -namespace Avalonia.Media -{ - public class GeometryCollection : Animatable, IList, IReadOnlyList - { - private List _inner; - - public GeometryCollection() => _inner = new List(); - public GeometryCollection(IEnumerable collection) => _inner = new List(collection); - public GeometryCollection(int capacity) => _inner = new List(capacity); - - public Geometry this[int index] - { - get => _inner[index]; - set => _inner[index] = value; - } - - public int Count => _inner.Count; - public bool IsReadOnly => false; - - public void Add(Geometry item) => _inner.Add(item); - public void Clear() => _inner.Clear(); - public bool Contains(Geometry item) => _inner.Contains(item); - public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex); - public IEnumerator GetEnumerator() => _inner.GetEnumerator(); - public int IndexOf(Geometry item) => _inner.IndexOf(item); - public void Insert(int index, Geometry item) => _inner.Insert(index, item); - public bool Remove(Geometry item) => _inner.Remove(item); - public void RemoveAt(int index) => _inner.RemoveAt(index); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Avalonia.Base/Media/DrawingCollection.cs b/src/Avalonia.Base/Media/DrawingCollection.cs new file mode 100644 index 0000000000..a76f7743cc --- /dev/null +++ b/src/Avalonia.Base/Media/DrawingCollection.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Avalonia.Collections; + +namespace Avalonia.Media +{ + public sealed class DrawingCollection : AvaloniaList + { + public DrawingCollection() + { + ResetBehavior = ResetBehavior.Remove; + } + + public DrawingCollection(IEnumerable items) : base(items) + { + ResetBehavior = ResetBehavior.Remove; + } + } +} diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index eeb6318ebd..603bb1c1c1 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -1,6 +1,10 @@ -using Avalonia.Collections; +using System; +using System.Collections.Generic; +using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -18,6 +22,14 @@ namespace Avalonia.Media public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect( + nameof(Children), + o => o.Children, + (o, v) => o.Children = v); + + private DrawingCollection _children = new DrawingCollection(); + public double Opacity { get => GetValue(OpacityProperty); @@ -42,8 +54,23 @@ namespace Avalonia.Media set => SetValue(OpacityMaskProperty, value); } + /// + /// Gets or sets the collection that contains the child geometries. + /// [Content] - public AvaloniaList Children { get; } = new AvaloniaList(); + public DrawingCollection Children + { + get => _children; + set + { + SetAndRaise(ChildrenProperty, ref _children, value); + } + } + + public DrawingContext Open() + { + return new DrawingContext(new DrawingGroupDrawingContext(this)); + } public override void Draw(DrawingContext context) { @@ -75,5 +102,394 @@ namespace Avalonia.Media return rect; } + + private class DrawingGroupDrawingContext : IDrawingContextImpl + { + private readonly DrawingGroup _drawingGroup; + private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); + + private Matrix _transform; + + private bool _disposed; + + // Root drawing created by this DrawingContext. + // + // If there is only a single child of the root DrawingGroup, _rootDrawing + // will reference the single child, and the root _currentDrawingGroup + // value will be null. Otherwise, _rootDrawing will reference the + // root DrawingGroup, and be the same value as the root _currentDrawingGroup. + // + // Either way, _rootDrawing always references the root drawing. + protected Drawing? _rootDrawing; + + // Current DrawingGroup that new children are added to + protected DrawingGroup? _currentDrawingGroup; + + // Previous values of _currentDrawingGroup + private Stack? _previousDrawingGroupStack; + + public DrawingGroupDrawingContext(DrawingGroup drawingGroup) + { + _drawingGroup = drawingGroup; + } + + public Matrix Transform + { + get => _transform; + set + { + _transform = value; + PushTransform(new MatrixTransform(value)); + } + } + + public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) + { + if ((brush == null) && (pen == null)) + { + return; + } + + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateEllipseGeometry(rect); + + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); + } + + public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + if (((brush == null) && (pen == null)) || (geometry == null)) + { + return; + } + + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); + } + + public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + { + if (foreground == null || glyphRun == null) + { + return; + } + + // Add a GlyphRunDrawing to the Drawing graph + GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing + { + Foreground = foreground, + GlyphRun = glyphRun, + }; + + // Add Drawing to the Drawing graph + AddDrawing(glyphRunDrawing); + } + + public void DrawLine(IPen pen, Point p1, Point p2) + { + if (pen == null) + { + return; + } + + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2); + + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry)); + } + + public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) + { + if ((brush == null) && (pen == null)) + { + return; + } + + // Instantiate the geometry + var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect); + + // Add Drawing to the Drawing graph + AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); + } + + public void Clear(Color color) + { + throw new NotImplementedException(); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) + { + throw new NotImplementedException(); + } + + public void Custom(ICustomDrawOperation custom) + { + throw new NotImplementedException(); + } + + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + throw new NotImplementedException(); + } + + public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + throw new NotImplementedException(); + } + + public void PopBitmapBlendMode() + { + throw new NotImplementedException(); + } + + public void PopClip() + { + throw new NotImplementedException(); + } + + public void PopGeometryClip() + { + throw new NotImplementedException(); + } + + public void PopOpacity() + { + throw new NotImplementedException(); + } + + public void PopOpacityMask() + { + throw new NotImplementedException(); + } + + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + throw new NotImplementedException(); + } + + public void PushClip(Rect clip) + { + throw new NotImplementedException(); + } + + public void PushClip(RoundedRect clip) + { + throw new NotImplementedException(); + } + + public void PushGeometryClip(IGeometryImpl clip) + { + throw new NotImplementedException(); + } + + public void PushOpacity(double opacity) + { + throw new NotImplementedException(); + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + // Dispose may be called multiple times without throwing + // an exception. + if (!_disposed) + { + // Match any outstanding Push calls with a Pop + if (_previousDrawingGroupStack != null) + { + int stackCount = _previousDrawingGroupStack.Count; + for (int i = 0; i < stackCount; i++) + { + Pop(); + } + } + + // Call CloseCore with the root DrawingGroup's children + DrawingCollection rootChildren; + + if (_currentDrawingGroup != null) + { + // If we created a root DrawingGroup because multiple elements + // exist at the root level, provide it's Children collection + // directly. + rootChildren = _currentDrawingGroup.Children; + } + else + { + // Create a new DrawingCollection if we didn't create a + // root DrawingGroup because the root level only contained + // a single child. + // + // This collection is needed by DrawingGroup.Open because + // Open always replaces it's Children collection. It isn't + // strictly needed for Append, but always using a collection + // simplifies the TransactionalAppend implementation (i.e., + // a seperate implemention isn't needed for a single element) + rootChildren = new DrawingCollection(); + + // + // We may need to opt-out of inheritance through the new Freezable. + // This is controlled by this.CanBeInheritanceContext. + // + if (_rootDrawing != null) + { + rootChildren.Add(_rootDrawing); + } + } + + // Inform our derived classes that Close was called + _drawingGroup.Children = rootChildren; + + _disposed = true; + } + } + + /// + /// Pop + /// + private void Pop() + { + // Verify that Pop hasn't been called too many times + if ((_previousDrawingGroupStack == null) || (_previousDrawingGroupStack.Count == 0)) + { + throw new InvalidOperationException("DrawingGroupStack count missmatch."); + } + + // Restore the previous value of the current drawing group + _currentDrawingGroup = _previousDrawingGroupStack.Pop(); + } + + /// + /// PushTransform - + /// Push a Transform which will apply to all drawing operations until the corresponding + /// Pop. + /// + /// The Transform to push. + private void PushTransform(Transform transform) + { + // Instantiate a new drawing group and set it as the _currentDrawingGroup + var drawingGroup = PushNewDrawingGroup(); + + // Set the transform on the new DrawingGroup + drawingGroup.Transform = transform; + } + + /// + /// Creates a new DrawingGroup for a Push* call by setting the + /// _currentDrawingGroup to a newly instantiated DrawingGroup, + /// and saving the previous _currentDrawingGroup value on the + /// _previousDrawingGroupStack. + /// + private DrawingGroup PushNewDrawingGroup() + { + // Instantiate a new drawing group + DrawingGroup drawingGroup = new DrawingGroup(); + + // Add it to the drawing graph, like any other Drawing + AddDrawing(drawingGroup); + + // Lazily allocate the stack when it is needed because many uses + // of DrawingDrawingContext will have a depth of one. + if (null == _previousDrawingGroupStack) + { + _previousDrawingGroupStack = new Stack(2); + } + + // Save the previous _currentDrawingGroup value. + // + // If this is the first call, the value of _currentDrawingGroup + // will be null because AddDrawing doesn't create a _currentDrawingGroup + // for the first drawing. Having null on the stack is valid, and simply + // denotes that this new DrawingGroup is the first child in the root + // DrawingGroup. It is also possible for the first value on the stack + // to be non-null, which means that the root DrawingGroup has other + // children. + _previousDrawingGroupStack.Push(_currentDrawingGroup); + + // Set this drawing group as the current one so that subsequent drawing's + // are added as it's children until Pop is called. + _currentDrawingGroup = drawingGroup; + + return drawingGroup; + } + + /// + /// Contains the functionality common to GeometryDrawing operations of + /// instantiating the GeometryDrawing, setting it's Freezable state, + /// and Adding it to the Drawing Graph. + /// + private void AddNewGeometryDrawing(IBrush? brush, IPen? pen, Geometry? geometry) + { + if (geometry == null) + { + throw new ArgumentNullException(nameof(geometry)); + } + + // Instantiate the GeometryDrawing + GeometryDrawing geometryDrawing = new GeometryDrawing + { + // We may need to opt-out of inheritance through the new Freezable. + // This is controlled by this.CanBeInheritanceContext. + Brush = brush, + Pen = pen, + Geometry = geometry + }; + + // Add it to the drawing graph + AddDrawing(geometryDrawing); + } + + /// + /// Adds a new Drawing to the DrawingGraph. + /// + /// This method avoids creating a DrawingGroup for the common case + /// where only a single child exists in the root DrawingGroup. + /// + private void AddDrawing(Drawing newDrawing) + { + if (newDrawing == null) + { + throw new ArgumentNullException(nameof(newDrawing)); + } + + if (_rootDrawing == null) + { + // When a DrawingGroup is set, it should be made the root if + // a root drawing didnt exist. + Contract.Requires(_currentDrawingGroup == null); + + // If this is the first Drawing being added, avoid creating a DrawingGroup + // and set this drawing as the root drawing. This optimizes the common + // case where only a single child exists in the root DrawingGroup. + _rootDrawing = newDrawing; + } + else if (_currentDrawingGroup == null) + { + // When the second drawing is added at the root level, set a + // DrawingGroup as the root and add both drawings to it. + + // Instantiate the DrawingGroup + _currentDrawingGroup = new DrawingGroup(); + + // Add both Children + _currentDrawingGroup.Children.Add(_rootDrawing); + _currentDrawingGroup.Children.Add(newDrawing); + + // Set the new DrawingGroup as the current + _rootDrawing = _currentDrawingGroup; + } + else + { + // If there already is a current drawing group, then simply add + // the new drawing too it. + _currentDrawingGroup.Children.Add(newDrawing); + } + } + } } } diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 7bdf59def0..5480336f84 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -1223,7 +1223,7 @@ namespace Avalonia.Media public double OverhangTrailing { get - { + { return BlackBoxMetrics.OverhangTrailing; } } @@ -1252,6 +1252,46 @@ namespace Avalonia.Media } } + /// + /// Obtains geometry for the text, including underlines and strikethroughs. + /// + /// The left top origin of the resulting geometry. + /// The geometry returned contains the combined geometry + /// of all of the glyphs, underlines and strikeThroughs that represent the formatted text. + /// Overlapping contours are merged by performing a Boolean union operation. + public Geometry? BuildGeometry(Point origin) + { + GeometryGroup? accumulatedGeometry = null; + var lineOrigin = origin; + + DrawingGroup drawing = new DrawingGroup(); + + using (var ctx = drawing.Open()) + { + using (var enumerator = GetEnumerator()) + { + while (enumerator.MoveNext()) + { + var currentLine = enumerator.Current; + + if (currentLine != null) + { + currentLine.Draw(ctx, lineOrigin); + + AdvanceLineOrigin(ref lineOrigin, currentLine); + } + } + } + } + + Transform? transform = new TranslateTransform(origin.X, origin.Y); + + // recursively go down the DrawingGroup to build up the geometry + CombineGeometryRecursive(drawing, ref transform, ref accumulatedGeometry); + + return accumulatedGeometry; + } + /// /// Draws the text object /// @@ -1284,6 +1324,93 @@ namespace Avalonia.Media } } + private void CombineGeometryRecursive(Drawing drawing, ref Transform? transform, ref GeometryGroup? accumulatedGeometry) + { + if (drawing is DrawingGroup group) + { + transform = group.Transform; + + if (group.Children is DrawingCollection children) + { + // recursively go down for DrawingGroup + foreach (var child in children) + { + CombineGeometryRecursive(child, ref transform, ref accumulatedGeometry); + } + } + } + else + { + if (drawing is GlyphRunDrawing glyphRunDrawing) + { + // process glyph run + var glyphRun = glyphRunDrawing.GlyphRun; + + if (glyphRun != null) + { + var glyphRunGeometry = glyphRun.BuildGeometry(); + + glyphRunGeometry.Transform = transform; + + if (accumulatedGeometry == null) + { + accumulatedGeometry = new GeometryGroup + { + FillRule = FillRule.NonZero + }; + } + + accumulatedGeometry.Children.Add(glyphRunGeometry); + } + } + else + { + if (drawing is GeometryDrawing geometryDrawing) + { + // process geometry (i.e. TextDecoration on the line) + var geometry = geometryDrawing.Geometry; + + if (geometry != null) + { + geometry.Transform = transform; + + if (geometry is LineGeometry lineGeometry) + { + // For TextDecoration drawn by DrawLine(), the geometry is a LineGeometry which has no + // bounding area. So this line won't show up. Work aroud it by increase the Bounding rect + // to be Pen's thickness + + var bounds = lineGeometry.Bounds; + + if (bounds.Height == 0) + { + bounds = bounds.WithHeight(geometryDrawing.Pen?.Thickness ?? 0); + } + else if (bounds.Width == 0) + { + bounds = bounds.WithWidth(geometryDrawing.Pen?.Thickness ?? 0); + } + + // convert the line geometry into a rectangle geometry + // we lost line cap info here + geometry = new RectangleGeometry(bounds); + } + + if (accumulatedGeometry == null) + { + accumulatedGeometry = new GeometryGroup + { + FillRule = FillRule.NonZero + }; + } + + accumulatedGeometry.Children.Add(geometry); + } + } + } + } + } + private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics) { var metrics = new CachedMetrics(); diff --git a/src/Avalonia.Base/Media/GeometryCollection.cs b/src/Avalonia.Base/Media/GeometryCollection.cs new file mode 100644 index 0000000000..2afa191dcf --- /dev/null +++ b/src/Avalonia.Base/Media/GeometryCollection.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections; + +#nullable enable + +namespace Avalonia.Media +{ + public sealed class GeometryCollection : AvaloniaList + { + public GeometryCollection() + { + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + Parent?.Invalidate(); + }, + x => + { + Parent?.Invalidate(); + }, + () => throw new NotSupportedException()); + } + + public GeometryCollection(IEnumerable items) : base(items) + { + ResetBehavior = ResetBehavior.Remove; + + this.ForEachItem( + x => + { + Parent?.Invalidate(); + }, + x => + { + Parent?.Invalidate(); + }, + () => throw new NotSupportedException()); + } + + public GeometryGroup? Parent { get; set; } + } +} diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 08e62df2cc..7df7d25954 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -21,14 +21,14 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty BrushProperty = - AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent); + public static readonly StyledProperty BrushProperty = + AvaloniaProperty.Register(nameof(Brush), Brushes.Transparent); /// /// Defines the property. /// - public static readonly StyledProperty PenProperty = - AvaloniaProperty.Register(nameof(Pen)); + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); /// /// Gets or sets the that describes the shape of this . @@ -43,7 +43,7 @@ namespace Avalonia.Media /// /// Gets or sets the used to fill the interior of the shape described by this . /// - public IBrush Brush + public IBrush? Brush { get => GetValue(BrushProperty); set => SetValue(BrushProperty, value); @@ -52,7 +52,7 @@ namespace Avalonia.Media /// /// Gets or sets the used to stroke this . /// - public IPen Pen + public IPen? Pen { get => GetValue(PenProperty); set => SetValue(PenProperty, value); diff --git a/src/Avalonia.Base/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs similarity index 69% rename from src/Avalonia.Base/GeometryGroup.cs rename to src/Avalonia.Base/Media/GeometryGroup.cs index b90c9c6d8a..b3b807c0a0 100644 --- a/src/Avalonia.Base/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Metadata; +using Avalonia.Metadata; using Avalonia.Platform; #nullable enable @@ -13,8 +10,8 @@ namespace Avalonia.Media /// public class GeometryGroup : Geometry { - public static readonly DirectProperty ChildrenProperty = - AvaloniaProperty.RegisterDirect ( + public static readonly DirectProperty ChildrenProperty = + AvaloniaProperty.RegisterDirect ( nameof(Children), o => o.Children, (o, v) => o.Children = v); @@ -22,20 +19,28 @@ namespace Avalonia.Media public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); - private GeometryCollection? _children; - private bool _childrenSet; + private GeometryCollection _children = new GeometryCollection(); /// /// Gets or sets the collection that contains the child geometries. /// [Content] - public GeometryCollection? Children + public GeometryCollection Children { - get => _children ??= (!_childrenSet ? new GeometryCollection() : null); + get => _children; set { + if(_children is GeometryCollection) + { + _children.Parent = null; + } + + if (value is GeometryCollection) + { + value.Parent = this; + } + SetAndRaise(ChildrenProperty, ref _children, value); - _childrenSet = true; } } @@ -52,16 +57,21 @@ namespace Avalonia.Media public override Geometry Clone() { var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; - if (_children?.Count > 0) + + if (_children.Count > 0) + { result.Children = new GeometryCollection(_children); + } + return result; } protected override IGeometryImpl? CreateDefiningGeometry() { - if (_children?.Count > 0) + if (_children.Count > 0) { var factory = AvaloniaLocator.Current.GetRequiredService(); + return factory.CreateGeometryGroup(FillRule, _children); } @@ -72,10 +82,18 @@ namespace Avalonia.Media { base.OnPropertyChanged(change); - if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) + switch (change.Property.Name) { - InvalidateGeometry(); + case nameof(FillRule): + case nameof(Children): + InvalidateGeometry(); + break; } } + + internal void Invalidate() + { + InvalidateGeometry(); + } } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 25c35a28e5..6f1fa03990 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -202,15 +202,9 @@ namespace Avalonia.Media { var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); + var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this); - var geometry = new PlatformGeometry(geometryImpl); - - var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale); - - geometry.Transform = transform; - - return geometry; + return new PlatformGeometry(geometryImpl); } /// diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index bfa9e70fce..e39a4e23df 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -62,9 +62,8 @@ namespace Avalonia.Platform /// Created a geometry implementation for the glyph run. /// /// The glyph run to build a geometry from. - /// The scaling of the produces geometry. /// The geometry returned contains the combined geometry of all glyphs in the glyph run. - IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); + IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun); /// /// Creates a renderer. diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 6471b87bfd..059a9a4e8f 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -114,10 +114,8 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - scale = Matrix.Identity; - return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 727677c82e..91fe4fc085 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,7 +62,7 @@ namespace Avalonia.Skia return new CombinedGeometryImpl(combineMode, g1, g2); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) { @@ -79,21 +79,29 @@ namespace Avalonia.Skia }; SKPath path = new SKPath(); - var matrix = SKMatrix.Identity; - var currentX = 0f; + var (currentX, currentY) = glyphRun.BaselineOrigin; - foreach (var glyph in glyphRun.GlyphIndices) + for (var i = 0; i < glyphRun.GlyphIndices.Count; i++) { - var p = skFont.GetGlyphPath(glyph); + var glyph = glyphRun.GlyphIndices[i]; + var glyphPath = skFont.GetGlyphPath(glyph); - path.AddPath(p, currentX, 0); + if (!glyphPath.IsEmpty) + { + path.AddPath(glyphPath, (float)currentX, (float)currentY); + } - currentX += p.Bounds.Right; + if (glyphRun.GlyphAdvances != null) + { + currentX += glyphRun.GlyphAdvances[i]; + } + else + { + currentX += glyphPath.Bounds.Right; + } } - scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY); - return new StreamGeometryImpl(path); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 04025f92e4..7f1af46e97 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -13,6 +13,7 @@ using Avalonia.Media.Imaging; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; using TextAlignment = Avalonia.Media.TextAlignment; +using SharpDX.Mathematics.Interop; namespace Avalonia { @@ -159,7 +160,7 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) { @@ -182,10 +183,23 @@ namespace Avalonia.Direct2D1 sink.Close(); } - scale = Matrix.Identity; + var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin; - return new StreamGeometryImpl(pathGeometry); - } + var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry( + Direct2D1Factory, + pathGeometry, + new RawMatrix3x2(1.0f, 0.0f, 0.0f, 1.0f, (float)baselineOriginX, (float)baselineOriginY)); + + return new TransformedGeometryWrapper(transformedGeometry); + } + + private class TransformedGeometryWrapper : GeometryImpl + { + public TransformedGeometryWrapper(SharpDX.Direct2D1.TransformedGeometry geometry) : base(geometry) + { + + } + } /// public IBitmapImpl LoadBitmap(string fileName) diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs index 8f80238903..a6078d7a4a 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs @@ -22,5 +22,23 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Null(target.Children); } + + [Fact] + public void Childrend_Change_Should_Raise_Changed() + { + var target = new GeometryGroup(); + + var children = new GeometryCollection(); + + target.Children = children; + + var isCalled = false; + + target.Changed += (s, e) => isCalled = true; + + children.Add(new StreamGeometry()); + + Assert.True(isCalled); + } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 183177495a..1f0b82b465 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -121,7 +121,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 51e75b6611..0193f5d772 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -117,7 +117,7 @@ namespace Avalonia.Benchmarks return new NullGlyphRun(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 6a8884a33a..1b0193bfdb 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -23,26 +23,28 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task Should_Render_GlyphRun_Geometry() { - Decorator target = new Decorator + var control = new GlyphRunGeometryControl { - Padding = new Thickness(8), - Width = 200, - Height = 100, - Child = new GlyphRunGeometryControl + [TextElement.ForegroundProperty] = new LinearGradientBrush { - [TextElement.ForegroundProperty] = new LinearGradientBrush - { - StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), - EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), - GradientStops = + StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), + GradientStops = { new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Blue, Offset = 1 } } - } } }; + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 190, + Height = 120, + Child = control + }; + await RenderToFile(target); CompareImages(); @@ -50,8 +52,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media public class GlyphRunGeometryControl : Control { - private readonly Geometry _geometry; - public GlyphRunGeometryControl() { var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; @@ -62,19 +62,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); - _geometry = glyphRun.BuildGeometry(); + Geometry = glyphRun.BuildGeometry(); } - protected override Size MeasureOverride(Size availableSize) - { - return _geometry.Bounds.Size; - } + public Geometry Geometry { get; } public override void Render(DrawingContext context) { var foreground = TextElement.GetForeground(this); - context.DrawGeometry(foreground, null, _geometry); + context.DrawGeometry(foreground, null, Geometry); } } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index c385e1c3eb..bf4ac9c1f6 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -122,10 +122,8 @@ namespace Avalonia.UnitTests return Mock.Of(); } - public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) { - scale = Matrix.Identity; - return Mock.Of(); } diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 7f1e0d29a1940266121b7f610f3763d3e08e4268..004265112f4595314a36a794e51da7ac7b9b8212 100644 GIT binary patch delta 5295 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 BhC=`V delta 5285 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 diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index a8f3aa927782575db62b7a370e9656a504898c29..407b67b8a0d49afd3cd0f616c45c01d9fd480222 100644 GIT binary patch delta 4087 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 p+z}0N_>)l(8wD-V?_P@>4QsYq6k$m669 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 JPDHLkV1m^t9DM)) From d64b8582738f2fac85c3cc86d2660d2d8f2a5428 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:30:24 +0200 Subject: [PATCH 2/9] Simplify GeometryGroup.Children setter --- src/Avalonia.Base/Media/GeometryGroup.cs | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index b3b807c0a0..dbd1b9c97c 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -14,7 +14,7 @@ namespace Avalonia.Media AvaloniaProperty.RegisterDirect ( nameof(Children), o => o.Children, - (o, v) => o.Children = v); + SetChildren); public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); @@ -29,18 +29,12 @@ namespace Avalonia.Media { get => _children; set - { - if(_children is GeometryCollection) - { - _children.Parent = null; - } - - if (value is GeometryCollection) - { - value.Parent = this; - } + { + _children.Parent = null; SetAndRaise(ChildrenProperty, ref _children, value); + + _children.Parent = this; } } @@ -66,6 +60,15 @@ namespace Avalonia.Media return result; } + private static void SetChildren(GeometryGroup geometryGroup, GeometryCollection children) + { + geometryGroup.Children.Parent = null; + + children.Parent = geometryGroup; + + geometryGroup.Children = children; + } + protected override IGeometryImpl? CreateDefiningGeometry() { if (_children.Count > 0) From a9b2dec6b643cb57ad54bcbe4a2dd74492db197c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:31:43 +0200 Subject: [PATCH 3/9] Remove redudant code --- src/Avalonia.Base/Media/GeometryGroup.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index dbd1b9c97c..cb47e61f11 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -29,12 +29,8 @@ namespace Avalonia.Media { get => _children; set - { - _children.Parent = null; - + { SetAndRaise(ChildrenProperty, ref _children, value); - - _children.Parent = this; } } From 9cfac4dd3fe2ed8b53cc785f846fe907ca90c57b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 16:46:59 +0200 Subject: [PATCH 4/9] FIx GeometryGroup.Children parent handling --- src/Avalonia.Base/Media/GeometryGroup.cs | 23 ++++++++++++------- .../Media/GeometryGroupTests.cs | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index cb47e61f11..0326e606f4 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -14,12 +14,20 @@ namespace Avalonia.Media AvaloniaProperty.RegisterDirect ( nameof(Children), o => o.Children, - SetChildren); + (o, v)=> o.Children = v); public static readonly StyledProperty FillRuleProperty = AvaloniaProperty.Register(nameof(FillRule)); - private GeometryCollection _children = new GeometryCollection(); + private GeometryCollection _children; + + public GeometryGroup() + { + _children = new GeometryCollection + { + Parent = this + }; + } /// /// Gets or sets the collection that contains the child geometries. @@ -30,7 +38,8 @@ namespace Avalonia.Media get => _children; set { - SetAndRaise(ChildrenProperty, ref _children, value); + OnChildrenChanged(_children, value); + SetAndRaise(ChildrenProperty, ref _children, value); } } @@ -56,13 +65,11 @@ namespace Avalonia.Media return result; } - private static void SetChildren(GeometryGroup geometryGroup, GeometryCollection children) + protected void OnChildrenChanged(GeometryCollection oldChildren, GeometryCollection newChildren) { - geometryGroup.Children.Parent = null; - - children.Parent = geometryGroup; + oldChildren.Parent = null; - geometryGroup.Children = children; + newChildren.Parent = this; } protected override IGeometryImpl? CreateDefiningGeometry() diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs index a6078d7a4a..91183cee4b 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs @@ -24,7 +24,7 @@ namespace Avalonia.Visuals.UnitTests.Media } [Fact] - public void Childrend_Change_Should_Raise_Changed() + public void Children_Change_Should_Raise_Changed() { var target = new GeometryGroup(); From 3dfad3bc79fcdceb680aabfa363ac528ef90fcef Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 10 Jun 2022 17:02:27 +0200 Subject: [PATCH 5/9] Remove redudant test --- .../Media/GeometryGroupTests.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs index 91183cee4b..fb4c35a1a8 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs @@ -13,16 +13,6 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.NotNull(target.Children); } - [Fact] - public void Children_Can_Be_Set_To_Null() - { - var target = new GeometryGroup(); - - target.Children = null; - - Assert.Null(target.Children); - } - [Fact] public void Children_Change_Should_Raise_Changed() { From 45d726e0c9ddac4e4a7c821e7589d51f6947ce7f Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Fri, 10 Jun 2022 19:22:50 +0200 Subject: [PATCH 6/9] Style improvements - primary OnCollectionChanged refactored (removed unnecessary cast and null checks in loops) --- src/Avalonia.Base/Styling/Styles.cs | 119 +++++++++++++++------------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 7c0bc4ad7f..903db5ffc7 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -17,7 +18,7 @@ namespace Avalonia.Styling IStyle, IResourceProvider { - private readonly AvaloniaList _styles = new AvaloniaList(); + private readonly AvaloniaList _styles = new(); private IResourceHost? _owner; private IResourceDictionary? _resources; private StyleCache? _cache; @@ -62,16 +63,18 @@ namespace Avalonia.Styling { value = value ?? throw new ArgumentNullException(nameof(Resources)); - if (Owner is object) + var currentOwner = Owner; + + if (currentOwner is not null) { - _resources?.RemoveOwner(Owner); + _resources?.RemoveOwner(currentOwner); } _resources = value; - if (Owner is object) + if (currentOwner is not null) { - _resources.AddOwner(Owner); + _resources.AddOwner(currentOwner); } } } @@ -89,7 +92,7 @@ namespace Avalonia.Styling foreach (var i in this) { - if (i is IResourceProvider p && p.HasResources) + if (i is IResourceProvider { HasResources: true }) { return true; } @@ -188,9 +191,9 @@ namespace Avalonia.Styling /// void IResourceProvider.AddOwner(IResourceHost owner) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); + ArgumentNullException.ThrowIfNull(owner); - if (Owner != null) + if (Owner is not null) { throw new InvalidOperationException("The Styles already has a owner."); } @@ -210,7 +213,7 @@ namespace Avalonia.Styling /// void IResourceProvider.RemoveOwner(IResourceHost owner) { - owner = owner ?? throw new ArgumentNullException(nameof(owner)); + ArgumentNullException.ThrowIfNull(owner); if (Owner == owner) { @@ -227,70 +230,72 @@ namespace Avalonia.Styling } } - private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + private static IReadOnlyList ToReadOnlyList(ICollection list) { - static IReadOnlyList ToReadOnlyList(IList list) + if (list is IReadOnlyList readOnlyList) { - if (list is IReadOnlyList) - { - return (IReadOnlyList)list; - } - else - { - var result = new T[list.Count]; - list.CopyTo(result, 0); - return result; - } + return readOnlyList; } - void Add(IList items) + var result = new T[list.Count]; + list.CopyTo(result, 0); + return result; + } + + private static void InternalAdd(IList items, IResourceHost owner, ref StyleCache? cache) + { + foreach (var resourceProvider in items.OfType()) { - for (var i = 0; i < items.Count; ++i) - { - var style = (IStyle)items[i]!; + resourceProvider.AddOwner(owner); + } - if (Owner is object && style is IResourceProvider resourceProvider) - { - resourceProvider.AddOwner(Owner); - } + if (items.Count > 0) + { + cache = null; + } - _cache = null; - } + (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); + } - (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); + private static void InternalRemove(IList items, IResourceHost owner, ref StyleCache? cache) + { + foreach (var resourceProvider in items.OfType()) + { + resourceProvider.RemoveOwner(owner); } - void Remove(IList items) + if (items.Count > 0) { - for (var i = 0; i < items.Count; ++i) - { - var style = (IStyle)items[i]!; - - if (Owner is object && style is IResourceProvider resourceProvider) - { - resourceProvider.RemoveOwner(Owner); - } + cache = null; + } - _cache = null; - } + (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); + } - (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); + private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + throw new InvalidOperationException("Reset should not be called on Styles."); } - switch (e.Action) + var currentOwner = Owner; + + if (currentOwner is not null) { - case NotifyCollectionChangedAction.Add: - Add(e.NewItems!); - break; - case NotifyCollectionChangedAction.Remove: - Remove(e.OldItems!); - break; - case NotifyCollectionChangedAction.Replace: - Remove(e.OldItems!); - Add(e.NewItems!); - break; - case NotifyCollectionChangedAction.Reset: - throw new InvalidOperationException("Reset should not be called on Styles."); + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Remove: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Replace: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; + } } CollectionChanged?.Invoke(this, e); From 1d0d1f20841440fd2b3385638aa0f24e8d9825d3 Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Fri, 10 Jun 2022 19:35:14 +0200 Subject: [PATCH 7/9] compile fix - ArgumentNullException.ThrowIfNull (net standard 2.0 fail) --- src/Avalonia.Base/Styling/Styles.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 903db5ffc7..175068541b 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -191,7 +191,7 @@ namespace Avalonia.Styling /// void IResourceProvider.AddOwner(IResourceHost owner) { - ArgumentNullException.ThrowIfNull(owner); + owner = owner ?? throw new ArgumentNullException(nameof(owner)); if (Owner is not null) { @@ -213,7 +213,7 @@ namespace Avalonia.Styling /// void IResourceProvider.RemoveOwner(IResourceHost owner) { - ArgumentNullException.ThrowIfNull(owner); + owner = owner ?? throw new ArgumentNullException(nameof(owner)); if (Owner == owner) { From 76765f75856f32eae7a24c5ad3224b8d66725bf9 Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Sat, 11 Jun 2022 06:51:27 +0200 Subject: [PATCH 8/9] Old cache reset logic --- src/Avalonia.Base/Styling/Styles.cs | 51 +++++++++++++++-------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 175068541b..1d0be96ac9 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -242,34 +242,40 @@ namespace Avalonia.Styling return result; } - private static void InternalAdd(IList items, IResourceHost owner, ref StyleCache? cache) + private static void InternalAdd(IList items, IResourceHost? owner, ref StyleCache? cache) { - foreach (var resourceProvider in items.OfType()) + if (owner is not null) { - resourceProvider.AddOwner(owner); + foreach (var resourceProvider in items.OfType()) + { + resourceProvider.AddOwner(owner); + } + + (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); } if (items.Count > 0) { cache = null; } - - (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); } - private static void InternalRemove(IList items, IResourceHost owner, ref StyleCache? cache) + private static void InternalRemove(IList items, IResourceHost? owner, ref StyleCache? cache) { - foreach (var resourceProvider in items.OfType()) + if (owner is not null) { - resourceProvider.RemoveOwner(owner); + foreach (var resourceProvider in items.OfType()) + { + resourceProvider.RemoveOwner(owner); + } + + (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); } if (items.Count > 0) { cache = null; } - - (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); } private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) @@ -281,21 +287,18 @@ namespace Avalonia.Styling var currentOwner = Owner; - if (currentOwner is not null) + switch (e.Action) { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - InternalAdd(e.NewItems!, currentOwner, ref _cache); - break; - case NotifyCollectionChangedAction.Remove: - InternalRemove(e.OldItems!, currentOwner, ref _cache); - break; - case NotifyCollectionChangedAction.Replace: - InternalRemove(e.OldItems!, currentOwner, ref _cache); - InternalAdd(e.NewItems!, currentOwner, ref _cache); - break; - } + case NotifyCollectionChangedAction.Add: + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Remove: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + break; + case NotifyCollectionChangedAction.Replace: + InternalRemove(e.OldItems!, currentOwner, ref _cache); + InternalAdd(e.NewItems!, currentOwner, ref _cache); + break; } CollectionChanged?.Invoke(this, e); From bbe7d0abb255863ed05b7ea1eee32be47e09df37 Mon Sep 17 00:00:00 2001 From: Mario Uhlmann Date: Sat, 11 Jun 2022 13:23:44 +0200 Subject: [PATCH 9/9] foreach .. "items.OfType<" replaced with traditional for-loop --- src/Avalonia.Base/Styling/Styles.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Styling/Styles.cs b/src/Avalonia.Base/Styling/Styles.cs index 1d0be96ac9..e4c3371007 100644 --- a/src/Avalonia.Base/Styling/Styles.cs +++ b/src/Avalonia.Base/Styling/Styles.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -246,9 +245,12 @@ namespace Avalonia.Styling { if (owner is not null) { - foreach (var resourceProvider in items.OfType()) + for (var i = 0; i < items.Count; ++i) { - resourceProvider.AddOwner(owner); + if (items[i] is IResourceProvider provider) + { + provider.AddOwner(owner); + } } (owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); @@ -264,9 +266,12 @@ namespace Avalonia.Styling { if (owner is not null) { - foreach (var resourceProvider in items.OfType()) + for (var i = 0; i < items.Count; ++i) { - resourceProvider.RemoveOwner(owner); + if (items[i] is IResourceProvider provider) + { + provider.RemoveOwner(owner); + } } (owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items));