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]);
+ }
+ }
+}