From 1f985abaa61937420796ae20241fa4638c529970 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Nov 2016 22:38:13 +0100 Subject: [PATCH] Refactored VisualNode. It now has two collections: Children and DrawOperations. --- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 3 +- .../Properties/AssemblyInfo.cs | 2 + .../Rendering/DeferredRenderer.cs | 16 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 138 +++++++------- .../Rendering/SceneGraph/GeometryNode.cs | 2 +- .../{IGeometryNode.cs => IDrawOperation.cs} | 14 +- .../Rendering/SceneGraph/ISceneNode.cs | 25 --- .../Rendering/SceneGraph/IVisualNode.cs | 22 ++- .../Rendering/SceneGraph/ImageNode.cs | 2 +- .../Rendering/SceneGraph/LineNode.cs | 2 +- .../Rendering/SceneGraph/RectangleNode.cs | 2 +- .../Rendering/SceneGraph/Scene.cs | 20 +- .../Rendering/SceneGraph/SceneBuilder.cs | 33 +++- .../Rendering/SceneGraph/TextNode.cs | 2 +- .../Rendering/SceneGraph/VisualNode.cs | 173 ++++++++++++++--- .../Avalonia.Visuals.UnitTests.csproj | 2 + .../DeferredDrawingContextImplTests.cs | 179 ++++++++++++++++++ .../Rendering/SceneGraph/SceneBuilderTests.cs | 27 +-- .../Rendering/SceneGraph/VisualNodeTests.cs | 80 ++++++++ 19 files changed, 565 insertions(+), 179 deletions(-) rename src/Avalonia.Visuals/Rendering/SceneGraph/{IGeometryNode.cs => IDrawOperation.cs} (67%) delete mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs create mode 100644 tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 24dde24907..ab309ed29d 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -116,9 +116,8 @@ - + - diff --git a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs index 25f5aeb8f8..496d58ccfa 100644 --- a/src/Avalonia.Visuals/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Visuals/Properties/AssemblyInfo.cs @@ -2,9 +2,11 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using System.Runtime.CompilerServices; using Avalonia.Metadata; [assembly: AssemblyTitle("Avalonia.Visuals")] +[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")] [assembly: XmlnsDefinition("https://github.com/avaloniaui/mutable", "Avalonia.Media.Mutable")] \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index a7315fbb39..acfa180a39 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -76,17 +76,19 @@ namespace Avalonia.Rendering if (!clipBounds.IsEmpty) { - node.Render(context); + node.BeginRender(context); - foreach (var child in node.Children) + foreach (var operation in node.DrawOperations) { - var visualChild = child as IVisualNode; + operation.Render(context); + } - if (visualChild != null) - { - Render(context, visualChild, clipBounds); - } + foreach (var child in node.Children) + { + Render(context, child, clipBounds); } + + node.EndRender(context); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 2907832f0b..05cd91e893 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -2,16 +2,16 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Reactive.Disposables; using Avalonia.Media; using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - public class DeferredDrawingContextImpl : IDrawingContextImpl + internal class DeferredDrawingContextImpl : IDrawingContextImpl { - private Stack _stack = new Stack(); + private VisualNode _node; + private int _childIndex; + private int _drawOperationindex; public DeferredDrawingContextImpl() : this(new DirtyRects()) @@ -23,57 +23,47 @@ namespace Avalonia.Rendering.SceneGraph Dirty = dirty; } - public Matrix Transform { get; set; } - - private VisualNode Node => _stack.Peek().Node; + public Matrix Transform { get; set; } = Matrix.Identity; public DirtyRects Dirty { get; } - private int Index + public UpdateState BeginUpdate(VisualNode node) { - get { return _stack.Peek().Index; } - set { _stack.Peek().Index = value; } - } + Contract.Requires(node != null); - public IDisposable Begin(VisualNode node) - { - if (_stack.Count > 0) + if (_node != null) { - var next = NextNodeAs(); - - if (next == null || next != node) + if (_childIndex < _node.Children.Count) { - Add(node); + _node.ReplaceChild(_childIndex, node); } else { - ++Index; + _node.AddChild(node); } + + ++_childIndex; } - _stack.Push(new Frame(node)); - return Disposable.Create(Pop); + var state = new UpdateState(this, _node, _childIndex, _drawOperationindex); + _node = node; + _childIndex = _drawOperationindex = 0; + return state; } public void Dispose() { + // Nothing to do here as we allocate no unmanaged resources. } - public void TrimNodes() + public void TrimChildren() { - var frame = _stack.Peek(); - var children = frame.Node.Children; - var index = frame.Index; - - if (children.Count > index) - { - children.RemoveRange(index, children.Count - index); - } + _node.TrimChildren(_childIndex); } public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var next = NextNodeAs(); + var next = NextDrawAs(); if (next == null || !next.Equals(Transform, brush, pen, geometry)) { @@ -81,13 +71,13 @@ namespace Avalonia.Rendering.SceneGraph } else { - ++Index; + ++_drawOperationindex; } } public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { - var next = NextNodeAs(); + var next = NextDrawAs(); if (next == null || !next.Equals(Transform, source, opacity, sourceRect, destRect)) { @@ -95,13 +85,13 @@ namespace Avalonia.Rendering.SceneGraph } else { - ++Index; + ++_drawOperationindex; } } public void DrawLine(Pen pen, Point p1, Point p2) { - var next = NextNodeAs(); + var next = NextDrawAs(); if (next == null || !next.Equals(Transform, pen, p1, p2)) { @@ -109,13 +99,13 @@ namespace Avalonia.Rendering.SceneGraph } else { - ++Index; + ++_drawOperationindex; } } public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) { - var next = NextNodeAs(); + var next = NextDrawAs(); if (next == null || !next.Equals(Transform, null, pen, rect, cornerRadius)) { @@ -123,13 +113,13 @@ namespace Avalonia.Rendering.SceneGraph } else { - ++Index; + ++_drawOperationindex; } } public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - var next = NextNodeAs(); + var next = NextDrawAs(); if (next == null || !next.Equals(Transform, foreground, origin, text)) { @@ -137,13 +127,13 @@ namespace Avalonia.Rendering.SceneGraph } else { - ++Index; + ++_drawOperationindex; } } public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) { - var next = NextNodeAs(); + var next = NextDrawAs(); if (next == null || !next.Equals(Transform, brush, null, rect, cornerRadius)) { @@ -151,7 +141,7 @@ namespace Avalonia.Rendering.SceneGraph } else { - ++Index; + ++_drawOperationindex; } } @@ -195,51 +185,57 @@ namespace Avalonia.Rendering.SceneGraph // TODO: Implement } - private void Add(ISceneNode node) + public struct UpdateState : IDisposable { - var index = Index; - - if (index < Node.Children.Count) - { - Node.Children[index] = node; - } - else + public UpdateState( + DeferredDrawingContextImpl owner, + VisualNode node, + int childIndex, + int drawOperationIndex) { - Node.Children.Add(node); + Owner = owner; + Node = node; + ChildIndex = childIndex; + DrawOperationIndex = drawOperationIndex; } - ++Index; - } - - private T NextNodeAs() where T : class, ISceneNode - { - return Index < Node.Children.Count ? Node.Children[Index] as T : null; - } - - private void Pop() - { - foreach (var child in Node.Children) + public void Dispose() { - var geometry = child as IGeometryNode; + Owner._node.TrimDrawOperations(Owner._drawOperationindex); - if (geometry != null) + foreach (var operation in Owner._node.DrawOperations) { - Dirty.Add(geometry.Bounds); + Owner.Dirty.Add(operation.Bounds); } + + Owner._node = Node; + Owner._childIndex = ChildIndex; + Owner._drawOperationindex = DrawOperationIndex; } - _stack.Pop(); + public DeferredDrawingContextImpl Owner { get; } + public VisualNode Node { get; } + public int ChildIndex { get; } + public int DrawOperationIndex { get; } } - class Frame + private void Add(IDrawOperation node) { - public Frame(VisualNode node) + if (_drawOperationindex < _node.DrawOperations.Count) { - Node = node; + _node.ReplaceDrawOperation(_drawOperationindex, node); + } + else + { + _node.AddDrawOperation(node); } - public VisualNode Node { get; } - public int Index { get; set; } + ++_drawOperationindex; + } + + private T NextDrawAs() where T : class, IDrawOperation + { + return _drawOperationindex < _node.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as T : null; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 2371392880..987cbf3d27 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - public class GeometryNode : IGeometryNode + public class GeometryNode : IDrawOperation { public GeometryNode(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IGeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs similarity index 67% rename from src/Avalonia.Visuals/Rendering/SceneGraph/IGeometryNode.cs rename to src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs index 4a9daf0ec9..d7d634f1aa 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IGeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs @@ -2,14 +2,20 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Media; namespace Avalonia.Rendering.SceneGraph { /// /// Represents a node in the low-level scene graph that represents geometry. /// - public interface IGeometryNode : ISceneNode + public interface IDrawOperation { + /// + /// Gets the bounds of the visible content in the node. + /// + Rect Bounds { get; } + /// /// Hit test the geometry in this node. /// @@ -20,5 +26,11 @@ namespace Avalonia.Rendering.SceneGraph /// to hit test children they must be hit tested manually. /// bool HitTest(Point p); + + /// + /// Renders the node to a drawing context. + /// + /// The drawing context. + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs deleted file mode 100644 index 1f9921a20a..0000000000 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Media; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Represents a node in the low-level scene graph. - /// - public interface ISceneNode - { - /// - /// Gets the bounds of the visible content in the node. - /// - Rect Bounds { get; } - - /// - /// Renders the node to a drawing context. - /// - /// The drawing context. - void Render(IDrawingContextImpl context); - } -} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index b52872d9c2..2824eedc99 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Avalonia.Media; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -10,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Represents a node in the low-level scene graph representing an . /// - public interface IVisualNode : ISceneNode + public interface IVisualNode { /// /// Gets the visual to which the node relates. @@ -44,7 +45,24 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the child scene graph nodes. /// - IReadOnlyList Children { get; } + IReadOnlyList Children { get; } + + /// + /// Gets the drawing operations for the visual. + /// + IReadOnlyList DrawOperations { get; } + + /// + /// Sets up the drawing context for rendering the node's geometry. + /// + /// The drawing context. + void BeginRender(IDrawingContextImpl context); + + /// + /// Resets the drawing context after rendering the node's geometry. + /// + /// The drawing context. + void EndRender(IDrawingContextImpl context); /// /// Hit test the geometry in this node. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index 86523c9a94..a11aa6f330 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - public class ImageNode : IGeometryNode + public class ImageNode : IDrawOperation { public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 346c740199..61ffada7d2 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -6,7 +6,7 @@ using Avalonia.Media; namespace Avalonia.Rendering.SceneGraph { - public class LineNode : IGeometryNode + public class LineNode : IDrawOperation { public LineNode(Matrix transform, Pen pen, Point p1, Point p2) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 0b9467018d..fd490e1cea 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -6,7 +6,7 @@ using Avalonia.Media; namespace Avalonia.Rendering.SceneGraph { - public class RectangleNode : IGeometryNode + public class RectangleNode : IDrawOperation { public RectangleNode(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 47804d95e6..940b90bf0f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -69,16 +69,7 @@ namespace Avalonia.Rendering.SceneGraph foreach (var child in source.Children) { - var visualNode = child as VisualNode; - - if (visualNode != null) - { - result.Children.Add(Clone(visualNode, result, index)); - } - else - { - result.Children.Add(child); - } + result.AddChild(Clone((VisualNode)child, result, index)); } return result; @@ -98,14 +89,9 @@ namespace Avalonia.Rendering.SceneGraph { for (var i = node.Children.Count - 1; i >= 0; --i) { - var visualChild = node.Children[i] as IVisualNode; - - if (visualChild != null) + foreach (var h in HitTest(node.Children[i], p, clip, filter)) { - foreach (var h in HitTest(visualChild, p, clip, filter)) - { - yield return h; - } + yield return h; } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 4baebdfa44..ca3a2585c7 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -78,7 +78,7 @@ namespace Avalonia.Rendering.SceneGraph { // The control has been removed so remove it from its parent and deindex the // node and its descendents. - ((VisualNode)node.Parent)?.Children.Remove(node); + ((VisualNode)node.Parent)?.RemoveChild(node); Deindex(scene, node, dirty); return true; } @@ -89,7 +89,7 @@ namespace Avalonia.Rendering.SceneGraph // The control has been removed so remove it from its parent and deindex the // node and its descendents. var trim = FindFirstDeadAncestor(scene, node); - ((VisualNode)trim.Parent).Children.Remove(trim); + ((VisualNode)trim.Parent).RemoveChild(trim); Deindex(scene, trim, dirty); return true; } @@ -148,12 +148,11 @@ namespace Avalonia.Rendering.SceneGraph m = renderTransform * m; - using (contextImpl.Begin(node)) + using (contextImpl.BeginUpdate(node)) using (context.PushPostTransform(m)) using (context.PushTransformContainer()) { - forceRecurse = forceRecurse || - node.Transform != contextImpl.Transform; + forceRecurse = forceRecurse || node.Transform != contextImpl.Transform; node.Transform = contextImpl.Transform; node.ClipBounds = (bounds * node.Transform).Intersect(clip); @@ -167,7 +166,11 @@ namespace Avalonia.Rendering.SceneGraph clip = clip.Intersect(node.ClipBounds); } - visual.Render(context); + try + { + visual.Render(context); + } + catch { } if (forceRecurse) { @@ -178,7 +181,11 @@ namespace Avalonia.Rendering.SceneGraph } node.SubTreeUpdated = true; - contextImpl.TrimNodes(); + contextImpl.TrimChildren(); + } + else if (node.OpacityChanged) + { + AddSubtreeBounds(node, contextImpl.Dirty); } } } @@ -198,7 +205,7 @@ namespace Avalonia.Rendering.SceneGraph foreach (var child in node.Children) { - var geometry = child as IGeometryNode; + var geometry = child as IDrawOperation; var visual = child as VisualNode; if (geometry != null) @@ -212,5 +219,15 @@ namespace Avalonia.Rendering.SceneGraph } } } + + private static void AddSubtreeBounds(VisualNode node, DirtyRects dirty) + { + dirty.Add(node.Bounds); + + foreach (var child in node.Children) + { + AddSubtreeBounds((VisualNode)child, dirty); + } + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index 3c59960c52..681bb23423 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -7,7 +7,7 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - public class TextNode : IGeometryNode + public class TextNode : IDrawOperation { public TextNode(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 7bd6039a62..d9a1c64cae 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -11,9 +11,16 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the low-level scene graph representing an . /// - public class VisualNode : IVisualNode + internal class VisualNode : IVisualNode { + private static readonly IReadOnlyList EmptyChildren = new IVisualNode[0]; + private static readonly IReadOnlyList EmptyDrawOperations = new IDrawOperation[0]; + private Rect? _bounds; + private double _opacity; + private List _children; + private List _drawOperations; + private bool _drawOperationsCloned; /// /// Initializes a new instance of the class. @@ -32,7 +39,6 @@ namespace Avalonia.Rendering.SceneGraph Visual = visual; Parent = parent; - Children = new List(); } /// @@ -57,28 +63,122 @@ namespace Avalonia.Rendering.SceneGraph public Geometry GeometryClip { get; set; } /// - /// Gets or sets the opacity of the scnee graph node. + /// Gets or sets the opacity of the scene graph node. /// - public double Opacity { get; set; } + public double Opacity + { + get { return _opacity; } + set + { + if (_opacity != value) + { + _opacity = value; + OpacityChanged = true; + } + } + } /// /// Gets or sets the opacity mask for the scnee graph node. /// public IBrush OpacityMask { get; set; } - /// - /// Gets the child scene graph nodes. - /// - public List Children { get; } - /// /// Gets a value indicating whether this node in the scene graph has already /// been updated in the current update pass. /// public bool SubTreeUpdated { get; set; } + /// + /// Gets a value indicating whether the property has changed. + /// + public bool OpacityChanged { get; private set; } + + /// + public IReadOnlyList Children => _children ?? EmptyChildren; + /// - IReadOnlyList IVisualNode.Children => Children; + public IReadOnlyList DrawOperations => _drawOperations ?? EmptyDrawOperations; + + /// + /// Adds a child to the collection. + /// + /// The child to add. + public void AddChild(IVisualNode child) + { + EnsureChildrenCreated(); + _children.Add(child); + } + + /// + /// Adds an operation to the collection. + /// + /// The operation to add. + public void AddDrawOperation(IDrawOperation operation) + { + EnsureDrawOperationsCreated(); + _drawOperations.Add(operation); + } + + /// + /// Removes a child from the collection. + /// + /// The child to remove. + public void RemoveChild(IVisualNode child) + { + EnsureChildrenCreated(); + _children.Remove(child); + } + + /// + /// Replaces a child in the collection. + /// + /// The child to be replaced. + /// The child to add. + public void ReplaceChild(int index, IVisualNode node) + { + EnsureChildrenCreated(); + _children[index] = node; + } + + /// + /// Replaces an item in the collection. + /// + /// The opeation to be replaced. + /// The operation to add. + public void ReplaceDrawOperation(int index, IDrawOperation operation) + { + EnsureDrawOperationsCreated(); + _drawOperations[index] = operation; + } + + /// + /// Removes items in the collection from the specified index + /// to the end. + /// + /// The index of the first child to be removed. + public void TrimChildren(int first) + { + if (first < _children?.Count) + { + EnsureChildrenCreated(); + _children.RemoveRange(first, _children.Count - first); + } + } + + /// + /// Removes items in the collection from the specified index + /// to the end. + /// + /// The index of the first operation to be removed. + public void TrimDrawOperations(int first) + { + if (first < _drawOperations?.Count) + { + EnsureDrawOperationsCreated(); + _drawOperations.RemoveRange(first, _drawOperations.Count - first); + } + } /// /// Makes a copy of the node @@ -93,19 +193,19 @@ namespace Avalonia.Rendering.SceneGraph ClipBounds = ClipBounds, ClipToBounds = ClipToBounds, GeometryClip = GeometryClip, - Opacity = Opacity, + _opacity = Opacity, OpacityMask = OpacityMask, + _drawOperations = _drawOperations, + _drawOperationsCloned = true, }; } /// public bool HitTest(Point p) { - foreach (var child in Children) + foreach (var operation in DrawOperations) { - var geometry = child as IGeometryNode; - - if (geometry?.HitTest(p) == true) + if (operation.HitTest(p) == true) { return true; } @@ -114,7 +214,8 @@ namespace Avalonia.Rendering.SceneGraph return false; } - public void Render(IDrawingContextImpl context) + /// + public void BeginRender(IDrawingContextImpl context) { context.Transform = Transform; @@ -127,15 +228,11 @@ namespace Avalonia.Rendering.SceneGraph { context.PushClip(ClipBounds * Transform.Invert()); } + } - foreach (var child in Children) - { - if (!(child is IVisualNode)) - { - child.Render(context); - } - } - + /// + public void EndRender(IDrawingContextImpl context) + { if (ClipToBounds) { context.PopClip(); @@ -151,16 +248,34 @@ namespace Avalonia.Rendering.SceneGraph { var result = new Rect(); - foreach (var child in Children) + foreach (var operation in DrawOperations) { - if (!(child is IVisualNode)) - { - result = result.Union(child.Bounds); - } + result = result.Union(operation.Bounds); } _bounds = result; return result; } + + private void EnsureChildrenCreated() + { + if (_children == null) + { + _children = new List(); + } + } + + private void EnsureDrawOperationsCreated() + { + if (_drawOperations == null) + { + _drawOperations = new List(); + } + else if (_drawOperationsCloned) + { + _drawOperations = new List(_drawOperations); + _drawOperationsCloned = false; + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj index 7ee8f7b1d1..345c0ad52d 100644 --- a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj +++ b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj @@ -81,7 +81,9 @@ + + diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs new file mode 100644 index 0000000000..366b40f16b --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs @@ -0,0 +1,179 @@ +using System; +using System.Linq; +using Avalonia.Media; +using Avalonia.Rendering; +using Avalonia.Rendering.SceneGraph; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class DeferredDrawingContextImplTests + { + [Fact] + public void Should_Add_VisualNode() + { + var parent = new VisualNode(Mock.Of(), null); + var child = new VisualNode(Mock.Of(), null); + var target = new DeferredDrawingContextImpl(); + + target.BeginUpdate(parent); + target.BeginUpdate(child); + + Assert.Equal(1, parent.Children.Count); + Assert.Same(child, parent.Children[0]); + } + + [Fact] + public void Should_Not_Replace_Identical_VisualNode() + { + var parent = new VisualNode(Mock.Of(), null); + var child = new VisualNode(Mock.Of(), null); + + parent.AddChild(child); + + var target = new DeferredDrawingContextImpl(); + + target.BeginUpdate(parent); + target.BeginUpdate(child); + + Assert.Equal(1, parent.Children.Count); + Assert.Same(child, parent.Children[0]); + } + + [Fact] + public void Should_Replace_Different_VisualNode() + { + var parent = new VisualNode(Mock.Of(), null); + var child1 = new VisualNode(Mock.Of(), null); + var child2 = new VisualNode(Mock.Of(), null); + + parent.AddChild(child1); + + var target = new DeferredDrawingContextImpl(); + + target.BeginUpdate(parent); + target.BeginUpdate(child2); + + Assert.Equal(1, parent.Children.Count); + Assert.Same(child2, parent.Children[0]); + } + + [Fact] + public void TrimChildren_Should_Trim_Children() + { + var node = new VisualNode(Mock.Of(), null); + + node.AddChild(new VisualNode(Mock.Of(), node)); + node.AddChild(new VisualNode(Mock.Of(), node)); + node.AddChild(new VisualNode(Mock.Of(), node)); + node.AddChild(new VisualNode(Mock.Of(), node)); + + var target = new DeferredDrawingContextImpl(); + var child1 = new VisualNode(Mock.Of(), null); + var child2 = new VisualNode(Mock.Of(), null); + + target.BeginUpdate(node); + using (target.BeginUpdate(child1)) { } + using (target.BeginUpdate(child2)) { } + target.TrimChildren(); + + Assert.Equal(2, node.Children.Count); + } + + [Fact] + public void Should_Add_DrawOperations() + { + var node = new VisualNode(Mock.Of(), null); + var target = new DeferredDrawingContextImpl(); + + using (target.BeginUpdate(node)) + { + target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100)); + target.DrawRectangle(new Pen(Brushes.Green, 1), new Rect(0, 0, 100, 100)); + } + + Assert.Equal(2, node.DrawOperations.Count); + Assert.IsType(node.DrawOperations[0]); + Assert.IsType(node.DrawOperations[1]); + } + + [Fact] + public void Should_Not_Replace_Identical_DrawOperation() + { + var node = new VisualNode(Mock.Of(), null); + var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0); + var target = new DeferredDrawingContextImpl(); + + node.AddDrawOperation(operation); + + using (target.BeginUpdate(node)) + { + target.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100)); + } + + Assert.Equal(1, node.DrawOperations.Count); + Assert.Same(operation, node.DrawOperations.Single()); + + Assert.IsType(node.DrawOperations[0]); + } + + [Fact] + public void Should_Replace_Different_DrawOperation() + { + var node = new VisualNode(Mock.Of(), null); + var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0); + var target = new DeferredDrawingContextImpl(); + + node.AddDrawOperation(operation); + + using (target.BeginUpdate(node)) + { + target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100)); + } + + Assert.Equal(1, node.DrawOperations.Count); + Assert.NotSame(operation, node.DrawOperations.Single()); + + Assert.IsType(node.DrawOperations[0]); + } + + [Fact] + public void Should_Update_DirtyRects() + { + var node = new VisualNode(Mock.Of(), null); + var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), 0); + var dirtyRects = new DirtyRects(); + var target = new DeferredDrawingContextImpl(dirtyRects); + + using (target.BeginUpdate(node)) + { + target.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100)); + } + + Assert.Equal(new Rect(0, 0, 100, 100), dirtyRects.Single()); + } + + [Fact] + public void Should_Trim_DrawOperations() + { + var node = new VisualNode(Mock.Of(), null); + + node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 10, 100), 0)); + node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 20, 100), 0)); + node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 30, 100), 0)); + node.AddDrawOperation(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 40, 100), 0)); + + var target = new DeferredDrawingContextImpl(); + + using (target.BeginUpdate(node)) + { + target.FillRectangle(Brushes.Green, new Rect(0, 0, 10, 100)); + target.FillRectangle(Brushes.Blue, new Rect(0, 0, 20, 100)); + } + + Assert.Equal(2, node.DrawOperations.Count); + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 80997ecb5a..348a4424fc 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -45,17 +45,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var borderNode = (VisualNode)result.Root.Children[0]; Assert.Same(borderNode, result.FindNode(border)); Assert.Same(border, borderNode.Visual); - Assert.Equal(2, borderNode.Children.Count); + Assert.Equal(1, borderNode.Children.Count); + Assert.Equal(1, borderNode.DrawOperations.Count); - var backgroundNode = (RectangleNode)borderNode.Children[0]; + var backgroundNode = (RectangleNode)borderNode.DrawOperations[0]; Assert.Equal(Brushes.Red, backgroundNode.Brush); - var textBlockNode = (VisualNode)borderNode.Children[1]; + var textBlockNode = (VisualNode)borderNode.Children[0]; Assert.Same(textBlockNode, result.FindNode(textBlock)); Assert.Same(textBlock, textBlockNode.Visual); - Assert.Equal(1, textBlockNode.Children.Count); + Assert.Equal(1, textBlockNode.DrawOperations.Count); - var textNode = (TextNode)textBlockNode.Children[0]; + var textNode = (TextNode)textBlockNode.DrawOperations[0]; Assert.NotNull(textNode.Text); } } @@ -243,7 +244,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph SceneBuilder.UpdateAll(initial); var initialBackgroundNode = initial.FindNode(border).Children[0]; - var initialTextNode = initial.FindNode(textBlock).Children[0]; + var initialTextNode = initial.FindNode(textBlock).DrawOperations[0]; Assert.NotNull(initialBackgroundNode); Assert.NotNull(initialTextNode); @@ -256,14 +257,14 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph var borderNode = (VisualNode)result.Root.Children[0]; Assert.Same(border, borderNode.Visual); - var backgroundNode = (RectangleNode)borderNode.Children[0]; + var backgroundNode = (RectangleNode)borderNode.DrawOperations[0]; Assert.NotSame(initialBackgroundNode, backgroundNode); Assert.Equal(Brushes.Green, backgroundNode.Brush); - var textBlockNode = (VisualNode)borderNode.Children[1]; + var textBlockNode = (VisualNode)borderNode.Children[0]; Assert.Same(textBlock, textBlockNode.Visual); - var textNode = (TextNode)textBlockNode.Children[0]; + var textNode = (TextNode)textBlockNode.DrawOperations[0]; Assert.Same(initialTextNode, textNode); } } @@ -306,9 +307,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.False(SceneBuilder.Update(result, canvas)); var borderNode = (VisualNode)result.Root.Children[0]; - Assert.Equal(2, borderNode.Children.Count); + Assert.Equal(1, borderNode.Children.Count); + Assert.Equal(1, borderNode.DrawOperations.Count); - var decoratorNode = (VisualNode)borderNode.Children[1]; + var decoratorNode = (VisualNode)borderNode.Children[0]; Assert.Same(decorator, decoratorNode.Visual); Assert.Same(decoratorNode, result.FindNode(decorator)); @@ -356,7 +358,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph Assert.False(SceneBuilder.Update(result, canvas)); var borderNode = (VisualNode)result.Root.Children[0]; - Assert.Equal(1, borderNode.Children.Count); + Assert.Equal(0, borderNode.Children.Count); + Assert.Equal(1, borderNode.DrawOperations.Count); Assert.Null(result.FindNode(decorator)); } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs new file mode 100644 index 0000000000..3d2e780e0b --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs @@ -0,0 +1,80 @@ +using System; +using Avalonia.Rendering.SceneGraph; +using Avalonia.VisualTree; +using Moq; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class VisualNodeTests + { + [Fact] + public void Empty_Children_Collections_Should_Be_Shared() + { + var node1 = new VisualNode(Mock.Of(), null); + var node2 = new VisualNode(Mock.Of(), null); + + Assert.Same(node1.Children, node2.Children); + } + + [Fact] + public void Adding_Child_Should_Create_Collection() + { + var node = new VisualNode(Mock.Of(), null); + var collection = node.Children; + + node.AddChild(Mock.Of()); + + Assert.NotSame(collection, node.Children); + } + + [Fact] + public void Empty_DrawOperations_Collections_Should_Be_Shared() + { + var node1 = new VisualNode(Mock.Of(), null); + var node2 = new VisualNode(Mock.Of(), null); + + Assert.Same(node1.DrawOperations, node2.DrawOperations); + } + + [Fact] + public void Adding_DrawOperation_Should_Create_Collection() + { + var node = new VisualNode(Mock.Of(), null); + var collection = node.DrawOperations; + + node.AddDrawOperation(Mock.Of()); + + Assert.NotSame(collection, node.DrawOperations); + } + + [Fact] + public void Cloned_Nodes_Should_Share_DrawOperations_Collection() + { + var node1 = new VisualNode(Mock.Of(), null); + node1.AddDrawOperation(Mock.Of()); + + var node2 = node1.Clone(null); + + Assert.Same(node1.DrawOperations, node2.DrawOperations); + } + + [Fact] + public void Adding_DrawOperation_To_Cloned_Node_Should_Create_New_Collection() + { + var node1 = new VisualNode(Mock.Of(), null); + var operation1 = Mock.Of(); + node1.AddDrawOperation(operation1); + + var node2 = node1.Clone(null); + var operation2 = Mock.Of(); + node2.ReplaceDrawOperation(0, operation2); + + Assert.NotSame(node1.DrawOperations, node2.DrawOperations); + Assert.Equal(1, node1.DrawOperations.Count); + Assert.Equal(1, node2.DrawOperations.Count); + Assert.Same(operation1, node1.DrawOperations[0]); + Assert.Same(operation2, node2.DrawOperations[0]); + } + } +}