Browse Source

Refactored VisualNode.

It now has two collections: Children and DrawOperations.
scenegraph-after-breakage
Steven Kirk 10 years ago
parent
commit
1f985abaa6
  1. 3
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  2. 2
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  3. 16
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  4. 138
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  5. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  6. 14
      src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs
  7. 25
      src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs
  8. 22
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  9. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  10. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  11. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  12. 20
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  13. 33
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  14. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  15. 173
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  16. 2
      tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj
  17. 179
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  18. 27
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  19. 80
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs

3
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -116,9 +116,8 @@
<Compile Include="Rendering\DefaultRenderLoop.cs" />
<Compile Include="Rendering\SceneGraph\DeferredDrawingContextImpl.cs" />
<Compile Include="Rendering\SceneGraph\GeometryNode.cs" />
<Compile Include="Rendering\SceneGraph\IGeometryNode.cs" />
<Compile Include="Rendering\SceneGraph\IDrawOperation.cs" />
<Compile Include="Rendering\SceneGraph\ImageNode.cs" />
<Compile Include="Rendering\SceneGraph\ISceneNode.cs" />
<Compile Include="Rendering\SceneGraph\IVisualNode.cs" />
<Compile Include="Rendering\SceneGraph\LineNode.cs" />
<Compile Include="Rendering\SceneGraph\RectangleNode.cs" />

2
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")]

16
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);
}
}

138
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<Frame> _stack = new Stack<Frame>();
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<ArgumentNullException>(node != null);
public IDisposable Begin(VisualNode node)
{
if (_stack.Count > 0)
if (_node != null)
{
var next = NextNodeAs<VisualNode>();
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<GeometryNode>();
var next = NextDrawAs<GeometryNode>();
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<ImageNode>();
var next = NextDrawAs<ImageNode>();
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<LineNode>();
var next = NextDrawAs<LineNode>();
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<RectangleNode>();
var next = NextDrawAs<RectangleNode>();
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<TextNode>();
var next = NextDrawAs<TextNode>();
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<RectangleNode>();
var next = NextDrawAs<RectangleNode>();
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<T>() 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<T>() where T : class, IDrawOperation
{
return _drawOperationindex < _node.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as T : null;
}
}
}

2
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)
{

14
src/Avalonia.Visuals/Rendering/SceneGraph/IGeometryNode.cs → 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
{
/// <summary>
/// Represents a node in the low-level scene graph that represents geometry.
/// </summary>
public interface IGeometryNode : ISceneNode
public interface IDrawOperation
{
/// <summary>
/// Gets the bounds of the visible content in the node.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
@ -20,5 +26,11 @@ namespace Avalonia.Rendering.SceneGraph
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Render(IDrawingContextImpl context);
}
}

25
src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs

@ -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
{
/// <summary>
/// Represents a node in the low-level scene graph.
/// </summary>
public interface ISceneNode
{
/// <summary>
/// Gets the bounds of the visible content in the node.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Render(IDrawingContextImpl context);
}
}

22
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
/// <summary>
/// Represents a node in the low-level scene graph representing an <see cref="IVisual"/>.
/// </summary>
public interface IVisualNode : ISceneNode
public interface IVisualNode
{
/// <summary>
/// Gets the visual to which the node relates.
@ -44,7 +45,24 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Gets the child scene graph nodes.
/// </summary>
IReadOnlyList<ISceneNode> Children { get; }
IReadOnlyList<IVisualNode> Children { get; }
/// <summary>
/// Gets the drawing operations for the visual.
/// </summary>
IReadOnlyList<IDrawOperation> DrawOperations { get; }
/// <summary>
/// Sets up the drawing context for rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
void BeginRender(IDrawingContextImpl context);
/// <summary>
/// Resets the drawing context after rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
void EndRender(IDrawingContextImpl context);
/// <summary>
/// Hit test the geometry in this node.

2
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)
{

2
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)
{

2
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)
{

20
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;
}
}

33
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);
}
}
}
}

2
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)
{

173
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -11,9 +11,16 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// A node in the low-level scene graph representing an <see cref="IVisual"/>.
/// </summary>
public class VisualNode : IVisualNode
internal class VisualNode : IVisualNode
{
private static readonly IReadOnlyList<IVisualNode> EmptyChildren = new IVisualNode[0];
private static readonly IReadOnlyList<IDrawOperation> EmptyDrawOperations = new IDrawOperation[0];
private Rect? _bounds;
private double _opacity;
private List<IVisualNode> _children;
private List<IDrawOperation> _drawOperations;
private bool _drawOperationsCloned;
/// <summary>
/// Initializes a new instance of the <see cref="VisualNode"/> class.
@ -32,7 +39,6 @@ namespace Avalonia.Rendering.SceneGraph
Visual = visual;
Parent = parent;
Children = new List<ISceneNode>();
}
/// <inheritdoc/>
@ -57,28 +63,122 @@ namespace Avalonia.Rendering.SceneGraph
public Geometry GeometryClip { get; set; }
/// <summary>
/// Gets or sets the opacity of the scnee graph node.
/// Gets or sets the opacity of the scene graph node.
/// </summary>
public double Opacity { get; set; }
public double Opacity
{
get { return _opacity; }
set
{
if (_opacity != value)
{
_opacity = value;
OpacityChanged = true;
}
}
}
/// <summary>
/// Gets or sets the opacity mask for the scnee graph node.
/// </summary>
public IBrush OpacityMask { get; set; }
/// <summary>
/// Gets the child scene graph nodes.
/// </summary>
public List<ISceneNode> Children { get; }
/// <summary>
/// Gets a value indicating whether this node in the scene graph has already
/// been updated in the current update pass.
/// </summary>
public bool SubTreeUpdated { get; set; }
/// <summary>
/// Gets a value indicating whether the <see cref="Opacity"/> property has changed.
/// </summary>
public bool OpacityChanged { get; private set; }
/// <inheritdoc/>
public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren;
/// <inheritdoc/>
IReadOnlyList<ISceneNode> IVisualNode.Children => Children;
public IReadOnlyList<IDrawOperation> DrawOperations => _drawOperations ?? EmptyDrawOperations;
/// <summary>
/// Adds a child to the <see cref="Children"/> collection.
/// </summary>
/// <param name="child">The child to add.</param>
public void AddChild(IVisualNode child)
{
EnsureChildrenCreated();
_children.Add(child);
}
/// <summary>
/// Adds an operation to the <see cref="DrawOperations"/> collection.
/// </summary>
/// <param name="operation">The operation to add.</param>
public void AddDrawOperation(IDrawOperation operation)
{
EnsureDrawOperationsCreated();
_drawOperations.Add(operation);
}
/// <summary>
/// Removes a child from the <see cref="Children"/> collection.
/// </summary>
/// <param name="child">The child to remove.</param>
public void RemoveChild(IVisualNode child)
{
EnsureChildrenCreated();
_children.Remove(child);
}
/// <summary>
/// Replaces a child in the <see cref="Children"/> collection.
/// </summary>
/// <param name="index">The child to be replaced.</param>
/// <param name="node">The child to add.</param>
public void ReplaceChild(int index, IVisualNode node)
{
EnsureChildrenCreated();
_children[index] = node;
}
/// <summary>
/// Replaces an item in the <see cref="DrawOperations"/> collection.
/// </summary>
/// <param name="index">The opeation to be replaced.</param>
/// <param name="operation">The operation to add.</param>
public void ReplaceDrawOperation(int index, IDrawOperation operation)
{
EnsureDrawOperationsCreated();
_drawOperations[index] = operation;
}
/// <summary>
/// Removes items in the <see cref="Children"/> collection from the specified index
/// to the end.
/// </summary>
/// <param name="first">The index of the first child to be removed.</param>
public void TrimChildren(int first)
{
if (first < _children?.Count)
{
EnsureChildrenCreated();
_children.RemoveRange(first, _children.Count - first);
}
}
/// <summary>
/// Removes items in the <see cref="DrawOperations"/> collection from the specified index
/// to the end.
/// </summary>
/// <param name="first">The index of the first operation to be removed.</param>
public void TrimDrawOperations(int first)
{
if (first < _drawOperations?.Count)
{
EnsureDrawOperationsCreated();
_drawOperations.RemoveRange(first, _drawOperations.Count - first);
}
}
/// <summary>
/// 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,
};
}
/// <inheritdoc/>
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)
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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<IVisualNode>();
}
}
private void EnsureDrawOperationsCreated()
{
if (_drawOperations == null)
{
_drawOperations = new List<IDrawOperation>();
}
else if (_drawOperationsCloned)
{
_drawOperations = new List<IDrawOperation>(_drawOperations);
_drawOperationsCloned = false;
}
}
}
}

2
tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj

@ -81,7 +81,9 @@
<Compile Include="Media\PathMarkupParserTests.cs" />
<Compile Include="RelativeRectComparer.cs" />
<Compile Include="RelativeRectTests.cs" />
<Compile Include="Rendering\SceneGraph\DeferredDrawingContextImplTests.cs" />
<Compile Include="Rendering\SceneGraph\SceneBuilderTests.cs" />
<Compile Include="Rendering\SceneGraph\VisualNodeTests.cs" />
<Compile Include="ThicknessTests.cs" />
<Compile Include="Media\BrushTests.cs" />
<Compile Include="Media\ColorTests.cs" />

179
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<IVisual>(), null);
var child = new VisualNode(Mock.Of<IVisual>(), 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<IVisual>(), null);
var child = new VisualNode(Mock.Of<IVisual>(), 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<IVisual>(), null);
var child1 = new VisualNode(Mock.Of<IVisual>(), null);
var child2 = new VisualNode(Mock.Of<IVisual>(), 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<IVisual>(), null);
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node));
var target = new DeferredDrawingContextImpl();
var child1 = new VisualNode(Mock.Of<IVisual>(), null);
var child2 = new VisualNode(Mock.Of<IVisual>(), 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<IVisual>(), 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<RectangleNode>(node.DrawOperations[0]);
Assert.IsType<RectangleNode>(node.DrawOperations[1]);
}
[Fact]
public void Should_Not_Replace_Identical_DrawOperation()
{
var node = new VisualNode(Mock.Of<IVisual>(), 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<RectangleNode>(node.DrawOperations[0]);
}
[Fact]
public void Should_Replace_Different_DrawOperation()
{
var node = new VisualNode(Mock.Of<IVisual>(), 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<RectangleNode>(node.DrawOperations[0]);
}
[Fact]
public void Should_Update_DirtyRects()
{
var node = new VisualNode(Mock.Of<IVisual>(), 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<IVisual>(), 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);
}
}
}

27
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));
}

80
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<IVisual>(), null);
var node2 = new VisualNode(Mock.Of<IVisual>(), null);
Assert.Same(node1.Children, node2.Children);
}
[Fact]
public void Adding_Child_Should_Create_Collection()
{
var node = new VisualNode(Mock.Of<IVisual>(), null);
var collection = node.Children;
node.AddChild(Mock.Of<IVisualNode>());
Assert.NotSame(collection, node.Children);
}
[Fact]
public void Empty_DrawOperations_Collections_Should_Be_Shared()
{
var node1 = new VisualNode(Mock.Of<IVisual>(), null);
var node2 = new VisualNode(Mock.Of<IVisual>(), null);
Assert.Same(node1.DrawOperations, node2.DrawOperations);
}
[Fact]
public void Adding_DrawOperation_Should_Create_Collection()
{
var node = new VisualNode(Mock.Of<IVisual>(), null);
var collection = node.DrawOperations;
node.AddDrawOperation(Mock.Of<IDrawOperation>());
Assert.NotSame(collection, node.DrawOperations);
}
[Fact]
public void Cloned_Nodes_Should_Share_DrawOperations_Collection()
{
var node1 = new VisualNode(Mock.Of<IVisual>(), null);
node1.AddDrawOperation(Mock.Of<IDrawOperation>());
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<IVisual>(), null);
var operation1 = Mock.Of<IDrawOperation>();
node1.AddDrawOperation(operation1);
var node2 = node1.Clone(null);
var operation2 = Mock.Of<IDrawOperation>();
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]);
}
}
}
Loading…
Cancel
Save