Browse Source

Initial impl of incremental update to SceneGraph.

scenegraph-after-breakage
Steven Kirk 10 years ago
parent
commit
934e18c8ba
  1. 2
      src/Avalonia.Base/Threading/Dispatcher.cs
  2. 90
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  3. 46
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  4. 6
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  5. 18
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  6. 135
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  7. 25
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  8. 19
      src/Avalonia.Visuals/Visual.cs
  9. 11
      src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs
  10. 5
      tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject
  11. 220
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  12. 90
      tests/Avalonia.Visuals.UnitTests/VisualTests.cs
  13. 10
      tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

2
src/Avalonia.Base/Threading/Dispatcher.cs

@ -79,6 +79,8 @@ namespace Avalonia.Threading
/// </summary> /// </summary>
/// <param name="action">The method.</param> /// <param name="action">The method.</param>
/// <param name="priority">The priority with which to invoke the method.</param> /// <param name="priority">The priority with which to invoke the method.</param>
// TODO: The naming of this method is confusing: the Async suffix usually means return a task.
// Remove this and rename InvokeTaskAsync as InvokeAsync.
public void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) public void InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
{ {
_jobRunner?.Post(action, priority); _jobRunner?.Post(action, priority);

90
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.SceneGraph;
@ -16,6 +15,7 @@ namespace Avalonia.Rendering
private readonly IRenderRoot _root; private readonly IRenderRoot _root;
private Scene _scene; private Scene _scene;
private IRenderTarget _renderTarget; private IRenderTarget _renderTarget;
private List<IVisual> _dirty = new List<IVisual>();
private bool _needsUpdate; private bool _needsUpdate;
private bool _needsRender; private bool _needsRender;
@ -39,11 +39,14 @@ namespace Avalonia.Rendering
public void AddDirty(IVisual visual) public void AddDirty(IVisual visual)
{ {
if (!_needsUpdate) // If the root of the scene has no children, then the scene is being set up; don't
// bother filling the dirty list with every control in the window.
if (_scene.Root.Children.Count > 0)
{ {
_needsUpdate = true; _dirty.Add(visual);
Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render);
} }
_needsUpdate = true;
} }
public void Dispose() public void Dispose()
@ -63,31 +66,6 @@ namespace Avalonia.Rendering
public void Render(Rect rect) public void Render(Rect rect)
{ {
if (_renderTarget == null)
{
_renderTarget = _root.CreateRenderTarget();
}
try
{
_totalFrames++;
using (var context = _renderTarget.CreateDrawingContext())
{
_scene.Root.Render(context);
if (DrawFps)
{
RenderFps(context);
}
}
}
catch (RenderTargetCorruptedException ex)
{
Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
_renderTarget.Dispose();
_renderTarget = null;
}
} }
private void RenderFps(IDrawingContextImpl context) private void RenderFps(IDrawingContextImpl context)
@ -122,7 +100,22 @@ namespace Avalonia.Rendering
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
_scene = SceneBuilder.Update(_scene); var scene = _scene.Clone();
if (_dirty.Count > 0)
{
foreach (var visual in _dirty)
{
SceneBuilder.Update(scene, visual);
}
}
else
{
SceneBuilder.UpdateAll(scene);
}
_scene = scene;
_needsUpdate = false; _needsUpdate = false;
_needsRender = true; _needsRender = true;
_root.Invalidate(new Rect(_root.ClientSize)); _root.Invalidate(new Rect(_root.ClientSize));
@ -130,10 +123,39 @@ namespace Avalonia.Rendering
private void OnRenderLoopTick(object sender, EventArgs e) private void OnRenderLoopTick(object sender, EventArgs e)
{ {
//if (_needsRender) if (_needsUpdate)
//{ {
// _needsRender = false; Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render);
//} }
if (_needsRender)
{
if (_renderTarget == null)
{
_renderTarget = _root.CreateRenderTarget();
}
try
{
_totalFrames++;
using (var context = _renderTarget.CreateDrawingContext())
{
_scene.Root.Render(context);
if (DrawFps)
{
RenderFps(context);
}
}
}
catch (RenderTargetCorruptedException ex)
{
Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
_renderTarget.Dispose();
_renderTarget = null;
}
}
} }
} }
} }

46
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@ -25,30 +26,39 @@ namespace Avalonia.Rendering.SceneGraph
} }
public IDisposable Begin(VisualNode node) public IDisposable Begin(VisualNode node)
{
_stack.Push(new Frame(node));
return Disposable.Create(Pop);
}
public void Dispose()
{
}
public void AddChild(IVisualNode visualNode)
{ {
if (_stack.Count > 0) if (_stack.Count > 0)
{ {
var next = NextNodeAs<VisualNode>(); var next = NextNodeAs<VisualNode>();
if (next == null || next != visualNode) if (next == null || next != node)
{ {
Add(visualNode); Add(node);
} }
else else
{ {
++Index; ++Index;
} }
} }
_stack.Push(new Frame(node));
return Disposable.Create(Pop);
}
public void Dispose()
{
}
public void TrimNodes()
{
var frame = _stack.Peek();
var children = frame.Node.Children;
var index = frame.Index;
if (children.Count > index)
{
children.RemoveRange(index, children.Count - index);
}
} }
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
@ -196,17 +206,7 @@ namespace Avalonia.Rendering.SceneGraph
return Index < Node.Children.Count ? Node.Children[Index] as T : null; return Index < Node.Children.Count ? Node.Children[Index] as T : null;
} }
private void Pop() private void Pop() => _stack.Pop();
{
var frame = _stack.Pop();
var children = frame.Node.Children;
var index = frame.Index;
if (children.Count > index)
{
children.RemoveRange(index, children.Count - index);
}
}
class Frame class Frame
{ {

6
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@ -10,8 +10,10 @@ namespace Avalonia.Rendering.SceneGraph
public interface IVisualNode : ISceneNode public interface IVisualNode : ISceneNode
{ {
IVisual Visual { get; } IVisual Visual { get; }
Rect ClipBounds { get; set; } IVisualNode Parent { get; }
bool ClipToBounds { get; set; } Matrix Transform { get; }
Rect ClipBounds { get; }
bool ClipToBounds { get; }
IReadOnlyList<ISceneNode> Children { get; } IReadOnlyList<ISceneNode> Children { get; }
bool HitTest(Point p); bool HitTest(Point p);

18
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -12,8 +12,9 @@ namespace Avalonia.Rendering.SceneGraph
private Dictionary<IVisual, IVisualNode> _index; private Dictionary<IVisual, IVisualNode> _index;
public Scene(IVisual rootVisual) public Scene(IVisual rootVisual)
: this(new VisualNode(rootVisual), new Dictionary<IVisual, IVisualNode>()) : this(new VisualNode(rootVisual, null), new Dictionary<IVisual, IVisualNode>())
{ {
_index.Add(rootVisual, Root);
} }
internal Scene(VisualNode root, Dictionary<IVisual, IVisualNode> index) internal Scene(VisualNode root, Dictionary<IVisual, IVisualNode> index)
@ -28,6 +29,8 @@ namespace Avalonia.Rendering.SceneGraph
public void Add(IVisualNode node) public void Add(IVisualNode node)
{ {
Contract.Requires<ArgumentNullException>(node != null);
_index.Add(node.Visual, node); _index.Add(node.Visual, node);
} }
@ -51,9 +54,16 @@ namespace Avalonia.Rendering.SceneGraph
return HitTest(Root, p, null, filter); return HitTest(Root, p, null, filter);
} }
private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary<IVisual, IVisualNode> index) public void Remove(IVisualNode node)
{
Contract.Requires<ArgumentNullException>(node != null);
_index.Remove(node.Visual);
}
private VisualNode Clone(VisualNode source, IVisualNode parent, Dictionary<IVisual, IVisualNode> index)
{ {
var result = source.Clone(); var result = source.Clone(parent);
index.Add(result.Visual, result); index.Add(result.Visual, result);
@ -99,8 +109,6 @@ namespace Avalonia.Rendering.SceneGraph
} }
} }
dynamic d = node.Visual;
if (node.HitTest(p)) if (node.HitTest(p))
{ {
yield return node.Visual; yield return node.Visual;

135
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -11,32 +11,113 @@ namespace Avalonia.Rendering.SceneGraph
{ {
public static class SceneBuilder public static class SceneBuilder
{ {
public static Scene Update(Scene scene) public static void UpdateAll(Scene scene)
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
scene = scene.Clone();
using (var impl = new DeferredDrawingContextImpl()) using (var impl = new DeferredDrawingContextImpl())
using (var context = new DrawingContext(impl)) using (var context = new DrawingContext(impl))
{ {
Update(context, scene, scene.Root.Visual, null); Update(context, scene, (VisualNode)scene.Root, true);
} }
}
public static bool Update(Scene scene, IVisual visual)
{
Dispatcher.UIThread.VerifyAccess();
var node = (VisualNode)scene.FindNode(visual);
if (visual.VisualRoot != null)
{
if (visual.IsVisible)
{
// If the node isn't yet part of the scene, find the nearest ancestor that is.
node = node ?? FindExistingAncestor(scene, visual);
// We don't need to do anything if this part of the tree has already been fully
// updated.
if (node != null && !node.SubTreeUpdated)
{
// If the control we've been asked to update isn't part of the scene then
// we're carrying out an add operation, so recurse and add all the
// descendents too.
var recurse = node.Visual != visual;
using (var impl = new DeferredDrawingContextImpl())
using (var context = new DrawingContext(impl))
{
if (node.Parent != null)
{
context.PushPostTransform(node.Parent.Transform);
}
Update(context, scene, (VisualNode)node, recurse);
}
return scene; return true;
}
}
else
{
if (node != null)
{
// The control has been removed so remove it from its parent and deindex the
// node and its descendents.
((VisualNode)node.Parent)?.Children.Remove(node);
Deindex(scene, node);
return true;
}
}
}
else if (node != null)
{
// 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);
Deindex(scene, trim);
return true;
}
return false;
} }
private static void Update(DrawingContext context, Scene scene, IVisual visual, VisualNode parent) private static VisualNode FindExistingAncestor(Scene scene, IVisual visual)
{ {
var node = scene.FindNode(visual);
while (node == null && visual.IsVisible)
{
visual = visual.VisualParent;
node = scene.FindNode(visual);
}
return visual.IsVisible ? (VisualNode)node : null;
}
private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node)
{
var parent = node.Parent;
while (parent.Visual.VisualRoot == null)
{
node = parent;
parent = node.Parent;
}
return (VisualNode)node;
}
private static void Update(DrawingContext context, Scene scene, VisualNode node, bool forceRecurse)
{
var visual = node.Visual;
var opacity = visual.Opacity; var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds; var clipToBounds = visual.ClipToBounds;
var bounds = new Rect(visual.Bounds.Size); var bounds = new Rect(visual.Bounds.Size);
var node = (VisualNode)scene.FindNode(visual) ?? CreateNode(visual, scene, parent);
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
contextImpl.AddChild(node); if (visual.IsVisible)
if (visual.IsVisible && opacity > 0)
{ {
var m = Matrix.CreateTranslation(visual.Bounds.Position); var m = Matrix.CreateTranslation(visual.Bounds.Position);
@ -55,6 +136,9 @@ namespace Avalonia.Rendering.SceneGraph
using (context.PushPostTransform(m)) using (context.PushPostTransform(m))
using (context.PushTransformContainer()) using (context.PushTransformContainer())
{ {
forceRecurse = forceRecurse ||
node.Transform != contextImpl.Transform;
node.Transform = contextImpl.Transform; node.Transform = contextImpl.Transform;
node.ClipBounds = bounds * node.Transform; node.ClipBounds = bounds * node.Transform;
node.ClipToBounds = clipToBounds; node.ClipToBounds = clipToBounds;
@ -64,19 +148,42 @@ namespace Avalonia.Rendering.SceneGraph
visual.Render(context); visual.Render(context);
foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) if (forceRecurse)
{ {
Update(context, scene, child, node); foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
{
var childNode = scene.FindNode(child) ?? CreateNode(scene, child, node);
Update(context, scene, (VisualNode)childNode, forceRecurse);
}
node.SubTreeUpdated = true;
contextImpl.TrimNodes();
} }
} }
} }
} }
private static VisualNode CreateNode(IVisual visual, Scene scene, VisualNode parent) private static VisualNode CreateNode(Scene scene, IVisual visual, IVisualNode parent)
{ {
var node = new VisualNode(visual); var node = new VisualNode(visual, parent);
scene.Add(node); scene.Add(node);
return node; return node;
} }
private static void Deindex(Scene scene, VisualNode node)
{
scene.Remove(node);
node.SubTreeUpdated = true;
foreach (var child in node.Children)
{
var visualChild = child as VisualNode;
if (visualChild != null)
{
Deindex(scene, visualChild);
}
}
}
} }
} }

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

@ -10,13 +10,21 @@ namespace Avalonia.Rendering.SceneGraph
{ {
public class VisualNode : IVisualNode public class VisualNode : IVisualNode
{ {
public VisualNode(IVisual visual) public VisualNode(IVisual visual, IVisualNode parent)
{ {
Children = new List<ISceneNode>(); if (parent == null && visual.VisualParent != null)
{
throw new AvaloniaInternalException(
"Attempted to create root VisualNode for parented visual.");
}
Visual = visual; Visual = visual;
Parent = parent;
Children = new List<ISceneNode>();
} }
public IVisual Visual { get; } public IVisual Visual { get; }
public IVisualNode Parent { get; }
public Matrix Transform { get; set; } public Matrix Transform { get; set; }
public Rect ClipBounds { get; set; } public Rect ClipBounds { get; set; }
public bool ClipToBounds { get; set; } public bool ClipToBounds { get; set; }
@ -24,12 +32,21 @@ namespace Avalonia.Rendering.SceneGraph
public double Opacity { get; set; } public double Opacity { get; set; }
public IBrush OpacityMask { get; set; } public IBrush OpacityMask { get; set; }
public List<ISceneNode> Children { get; } public List<ISceneNode> Children { get; }
public bool SubTreeUpdated { get; set; }
IReadOnlyList<ISceneNode> IVisualNode.Children => Children; IReadOnlyList<ISceneNode> IVisualNode.Children => Children;
public VisualNode Clone() public VisualNode Clone(IVisualNode parent)
{ {
return new VisualNode(Visual); return new VisualNode(Visual, parent)
{
Transform = Transform,
ClipBounds = ClipBounds,
ClipToBounds = ClipToBounds,
GeometryClip = GeometryClip,
Opacity = Opacity,
OpacityMask = OpacityMask,
};
} }
public bool HitTest(Point p) public bool HitTest(Point p)

19
src/Avalonia.Visuals/Visual.cs

@ -88,6 +88,7 @@ namespace Avalonia
AvaloniaProperty.Register<Visual, int>(nameof(ZIndex)); AvaloniaProperty.Register<Visual, int>(nameof(ZIndex));
private Rect _bounds; private Rect _bounds;
private IRenderRoot _visualRoot;
private IVisual _visualParent; private IVisual _visualParent;
/// <summary> /// <summary>
@ -233,11 +234,7 @@ namespace Avalonia
/// <summary> /// <summary>
/// Gets the root of the visual tree, if the control is attached to a visual tree. /// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary> /// </summary>
protected IRenderRoot VisualRoot protected IRenderRoot VisualRoot => _visualRoot ?? (this as IRenderRoot);
{
get;
private set;
}
/// <summary> /// <summary>
/// Gets a value indicating whether this scene graph node is attached to a visual root. /// Gets a value indicating whether this scene graph node is attached to a visual root.
@ -326,7 +323,7 @@ namespace Avalonia
{ {
Logger.Verbose(LogArea.Visual, this, "Attached to visual tree"); Logger.Verbose(LogArea.Visual, this, "Attached to visual tree");
VisualRoot = e.Root; _visualRoot = e.Root;
if (RenderTransform != null) if (RenderTransform != null)
{ {
@ -334,6 +331,7 @@ namespace Avalonia
} }
OnAttachedToVisualTree(e); OnAttachedToVisualTree(e);
InvalidateVisual();
if (VisualChildren != null) if (VisualChildren != null)
{ {
@ -353,7 +351,7 @@ namespace Avalonia
{ {
Logger.Verbose(LogArea.Visual, this, "Detached from visual tree"); Logger.Verbose(LogArea.Visual, this, "Detached from visual tree");
VisualRoot = null; _visualRoot = null;
if (RenderTransform != null) if (RenderTransform != null)
{ {
@ -361,6 +359,7 @@ namespace Avalonia
} }
OnDetachedFromVisualTree(e); OnDetachedFromVisualTree(e);
e.Root?.Renderer?.AddDirty(this);
if (VisualChildren != null) if (VisualChildren != null)
{ {
@ -501,16 +500,16 @@ namespace Avalonia
var old = _visualParent; var old = _visualParent;
_visualParent = value; _visualParent = value;
if (VisualRoot != null) if (_visualRoot != null)
{ {
var e = new VisualTreeAttachmentEventArgs(VisualRoot); var e = new VisualTreeAttachmentEventArgs(old, VisualRoot);
OnDetachedFromVisualTreeCore(e); OnDetachedFromVisualTreeCore(e);
} }
if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
{ {
var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault(); var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var e = new VisualTreeAttachmentEventArgs(root); var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
OnAttachedToVisualTreeCore(e); OnAttachedToVisualTreeCore(e);
} }

11
src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs

@ -3,6 +3,7 @@
using System; using System;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia namespace Avalonia
{ {
@ -15,14 +16,22 @@ namespace Avalonia
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="VisualTreeAttachmentEventArgs"/> class. /// Initializes a new instance of the <see cref="VisualTreeAttachmentEventArgs"/> class.
/// </summary> /// </summary>
/// <param name="parent">The parent that the visual is being attached to or detached from.</param>
/// <param name="root">The root visual.</param> /// <param name="root">The root visual.</param>
public VisualTreeAttachmentEventArgs(IRenderRoot root) public VisualTreeAttachmentEventArgs(IVisual parent, IRenderRoot root)
{ {
Contract.Requires<ArgumentNullException>(parent != null);
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
Parent = parent;
Root = root; Root = root;
} }
/// <summary>
/// Gets the parent that the visual is being attached to or detached from.
/// </summary>
public IVisual Parent { get; }
/// <summary> /// <summary>
/// Gets the root of the visual tree that the visual is being attached to or detached from. /// Gets the root of the visual tree that the visual is being attached to or detached from.
/// </summary> /// </summary>

5
tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject

@ -7,7 +7,7 @@
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking> <AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking> <AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis> <AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely> <IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents> <RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents> <RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully> <PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
@ -23,5 +23,8 @@
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture> <UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState> <MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture> <BuildProcessArchitecture>x86</BuildProcessArchitecture>
<IgnoredTests>
<AllTestsSelector />
</IgnoredTests>
<HiddenWarnings>AbnormalReferenceResolution</HiddenWarnings> <HiddenWarnings>AbnormalReferenceResolution</HiddenWarnings>
</ProjectConfiguration> </ProjectConfiguration>

220
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@ -6,6 +6,7 @@ using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Xunit; using Xunit;
using Avalonia.Layout;
namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
{ {
@ -35,10 +36,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
tree.Measure(Size.Infinity); tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize)); tree.Arrange(new Rect(tree.DesiredSize));
var initial = new Scene(tree); var result = new Scene(tree);
var result = SceneBuilder.Update(initial); SceneBuilder.UpdateAll(result);
Assert.NotSame(initial, result);
Assert.Equal(1, result.Root.Children.Count); Assert.Equal(1, result.Root.Children.Count);
var borderNode = (VisualNode)result.Root.Children[0]; var borderNode = (VisualNode)result.Root.Children[0];
@ -59,6 +59,41 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
} }
} }
[Fact]
public void Should_Respect_Margin_For_ClipBounds()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Canvas canvas;
var tree = new TestRoot
{
Width = 200,
Height = 300,
Child = new Border
{
Margin = new Thickness(10, 20, 30, 40),
Child = canvas = new Canvas(),
}
};
tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize));
var result = new Scene(tree);
SceneBuilder.UpdateAll(result);
var canvasNode = result.FindNode(canvas);
Assert.Equal(new Rect(10, 20, 160, 240), canvasNode.ClipBounds);
// Initial ClipBounds are correct, make sure they're still correct after updating canvas.
result = result.Clone();
Assert.True(SceneBuilder.Update(result, canvas));
canvasNode = result.FindNode(canvas);
Assert.Equal(new Rect(10, 20, 160, 240), canvasNode.ClipBounds);
}
}
[Fact] [Fact]
public void Should_Respect_ZIndex() public void Should_Respect_ZIndex()
{ {
@ -84,7 +119,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
} }
}; };
var result = SceneBuilder.Update(new Scene(tree)); var result = new Scene(tree);
SceneBuilder.UpdateAll(result);
var panelNode = result.FindNode(tree.Child); var panelNode = result.FindNode(tree.Child);
var expected = new IVisual[] { back, front }; var expected = new IVisual[] { back, front };
@ -116,7 +152,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
tree.Measure(Size.Infinity); tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize)); tree.Arrange(new Rect(tree.DesiredSize));
var result = SceneBuilder.Update(new Scene(tree)); var result = new Scene(tree);
SceneBuilder.UpdateAll(result);
var targetNode = result.FindNode(target); var targetNode = result.FindNode(target);
Assert.Equal(new Rect(50, 50, 100, 100), targetNode.ClipBounds); Assert.Equal(new Rect(50, 50, 100, 100), targetNode.ClipBounds);
@ -148,7 +186,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
tree.Measure(Size.Infinity); tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize)); tree.Arrange(new Rect(tree.DesiredSize));
var initial = SceneBuilder.Update(new Scene(tree)); var initial = new Scene(tree);
SceneBuilder.UpdateAll(initial);
var initialBackgroundNode = initial.FindNode(border).Children[0]; var initialBackgroundNode = initial.FindNode(border).Children[0];
var initialTextNode = initial.FindNode(textBlock).Children[0]; var initialTextNode = initial.FindNode(textBlock).Children[0];
@ -156,9 +196,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Assert.NotNull(initialTextNode); Assert.NotNull(initialTextNode);
border.Background = Brushes.Green; border.Background = Brushes.Green;
var result = SceneBuilder.Update(initial);
Assert.NotSame(initial, result); var result = initial.Clone();
SceneBuilder.Update(result, border);
var borderNode = (VisualNode)result.Root.Children[0]; var borderNode = (VisualNode)result.Root.Children[0];
Assert.Same(border, borderNode.Visual); Assert.Same(border, borderNode.Visual);
@ -174,13 +215,64 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
} }
} }
[Fact]
public void Should_Update_When_Control_Added()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border;
var tree = new TestRoot
{
Width = 100,
Height = 100,
Child = border = new Border
{
Background = Brushes.Red,
}
};
Canvas canvas;
var decorator = new Decorator
{
Child = canvas = new Canvas(),
};
tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize));
var initial = new Scene(tree);
SceneBuilder.UpdateAll(initial);
border.Child = decorator;
var result = initial.Clone();
Assert.True(SceneBuilder.Update(result, decorator));
// Updating canvas should result in no-op as it should have been updated along
// with decorator as part of the add opeation.
Assert.False(SceneBuilder.Update(result, canvas));
var borderNode = (VisualNode)result.Root.Children[0];
Assert.Equal(2, borderNode.Children.Count);
var decoratorNode = (VisualNode)borderNode.Children[1];
Assert.Same(decorator, decoratorNode.Visual);
Assert.Same(decoratorNode, result.FindNode(decorator));
var canvasNode = (VisualNode)decoratorNode.Children[0];
Assert.Same(canvas, canvasNode.Visual);
Assert.Same(canvasNode, result.FindNode(canvas));
}
}
[Fact] [Fact]
public void Should_Update_When_Control_Removed() public void Should_Update_When_Control_Removed()
{ {
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{ {
Border border; Border border;
TextBlock textBlock; Decorator decorator;
Canvas canvas;
var tree = new TestRoot var tree = new TestRoot
{ {
Width = 100, Width = 100,
@ -188,9 +280,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Child = border = new Border Child = border = new Border
{ {
Background = Brushes.Red, Background = Brushes.Red,
Child = textBlock = new TextBlock Child = decorator = new Decorator
{ {
Text = "Hello World", Child = canvas = new Canvas()
} }
} }
}; };
@ -198,15 +290,115 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
tree.Measure(Size.Infinity); tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize)); tree.Arrange(new Rect(tree.DesiredSize));
var initial = SceneBuilder.Update(new Scene(tree)); var initial = new Scene(tree);
SceneBuilder.UpdateAll(initial);
border.Child = null; border.Child = null;
var result = SceneBuilder.Update(initial); var result = initial.Clone();
Assert.True(SceneBuilder.Update(result, decorator));
Assert.False(SceneBuilder.Update(result, canvas));
Assert.NotSame(initial, result);
var borderNode = (VisualNode)result.Root.Children[0]; var borderNode = (VisualNode)result.Root.Children[0];
Assert.Equal(1, borderNode.Children.Count); Assert.Equal(1, borderNode.Children.Count);
Assert.Null(result.FindNode(decorator));
}
}
[Fact]
public void Should_Update_When_Control_Made_Invisible()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Decorator decorator;
Border border;
Canvas canvas;
var tree = new TestRoot
{
Width = 100,
Height = 100,
Child = decorator = new Decorator
{
Child = border = new Border
{
Background = Brushes.Red,
Child = canvas = new Canvas(),
}
}
};
tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize));
var initial = new Scene(tree);
SceneBuilder.UpdateAll(initial);
border.IsVisible = false;
var result = initial.Clone();
Assert.True(SceneBuilder.Update(result, border));
Assert.False(SceneBuilder.Update(result, canvas));
var decoratorNode = (VisualNode)result.Root.Children[0];
Assert.Equal(0, decoratorNode.Children.Count);
Assert.Null(result.FindNode(border));
Assert.Null(result.FindNode(canvas));
}
}
[Fact]
public void Should_Update_Descendent_Tranform_When_Margin_Changed()
{
using (TestApplication())
{
Decorator decorator;
Border border;
Canvas canvas;
var tree = new TestRoot
{
Width = 100,
Height = 100,
Child = decorator = new Decorator
{
Margin = new Thickness(0, 10, 0, 0),
Child = border = new Border
{
Child = canvas = new Canvas(),
}
}
};
var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
layout.ExecuteInitialLayoutPass(tree);
var scene = new Scene(tree);
SceneBuilder.UpdateAll(scene);
var borderNode = scene.FindNode(border);
var canvasNode = scene.FindNode(canvas);
Assert.Equal(Matrix.CreateTranslation(0, 10), borderNode.Transform);
Assert.Equal(Matrix.CreateTranslation(0, 10), canvasNode.Transform);
decorator.Margin = new Thickness(0, 20, 0, 0);
layout.ExecuteLayoutPass();
scene = scene.Clone();
SceneBuilder.Update(scene, decorator);
borderNode = scene.FindNode(border);
canvasNode = scene.FindNode(canvas);
Assert.Equal(Matrix.CreateTranslation(0, 20), borderNode.Transform);
Assert.Equal(Matrix.CreateTranslation(0, 20), canvasNode.Transform);
} }
} }
private IDisposable TestApplication()
{
return UnitTestApplication.Start(
TestServices.MockPlatformRenderInterface.With(
layoutManager: new LayoutManager()));
}
} }
} }

90
tests/Avalonia.Visuals.UnitTests/VisualTests.cs

@ -5,8 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Rendering;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Visuals.UnitTests namespace Avalonia.Visuals.UnitTests
@ -73,8 +75,19 @@ namespace Avalonia.Visuals.UnitTests
var called1 = false; var called1 = false;
var called2 = false; var called2 = false;
child1.AttachedToVisualTree += (s, e) => called1 = true; child1.AttachedToVisualTree += (s, e) =>
child2.AttachedToVisualTree += (s, e) => called2 = true; {
Assert.Equal(e.Parent, root);
Assert.Equal(e.Root, root);
called1 = true;
};
child2.AttachedToVisualTree += (s, e) =>
{
Assert.Equal(e.Parent, root);
Assert.Equal(e.Root, root);
called2 = true;
};
root.Child = child1; root.Child = child1;
@ -92,14 +105,83 @@ namespace Avalonia.Visuals.UnitTests
var called2 = false; var called2 = false;
root.Child = child1; root.Child = child1;
child1.DetachedFromVisualTree += (s, e) => called1 = true;
child2.DetachedFromVisualTree += (s, e) => called2 = true; child1.DetachedFromVisualTree += (s, e) =>
{
Assert.Equal(e.Parent, root);
Assert.Equal(e.Root, root);
called1 = true;
};
child2.DetachedFromVisualTree += (s, e) =>
{
Assert.Equal(e.Parent, root);
Assert.Equal(e.Root, root);
called2 = true;
};
root.Child = null; root.Child = null;
Assert.True(called1); Assert.True(called1);
Assert.True(called2); Assert.True(called2);
} }
[Fact]
public void Root_Should_Retun_Self_As_VisualRoot()
{
var root = new TestRoot();
Assert.Same(root, ((IVisual)root).VisualRoot);
}
[Fact]
public void Descendents_Should_RetunVisualRoot()
{
var root = new TestRoot();
var child1 = new Decorator();
var child2 = new Decorator();
root.Child = child1;
child1.Child = child2;
Assert.Same(root, ((IVisual)child1).VisualRoot);
Assert.Same(root, ((IVisual)child2).VisualRoot);
}
[Fact]
public void Attaching_To_Visual_Tree_Should_Invalidate_Visual()
{
var renderer = new Mock<IRenderer>();
using (UnitTestApplication.Start(new TestServices(renderer: (root, loop) => renderer.Object)))
{
var child = new Decorator();
var root = new TestRoot();
root.Child = child;
renderer.Verify(x => x.AddDirty(child));
}
}
[Fact]
public void Detaching_From_Visual_Tree_Should_Invalidate_Visual()
{
var renderer = new Mock<IRenderer>();
using (UnitTestApplication.Start(new TestServices(renderer: (root, loop) => renderer.Object)))
{
var child = new Decorator();
var root = new TestRoot();
root.Child = child;
renderer.ResetCalls();
root.Child = null;
renderer.Verify(x => x.AddDirty(child));
}
}
[Fact] [Fact]
public void Adding_Already_Parented_Control_Should_Throw() public void Adding_Already_Parented_Control_Should_Throw()
{ {

10
tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs

@ -349,12 +349,14 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
{ {
(target = new Border() (target = new Border()
{ {
Name = "b1",
Width = 100, Width = 100,
Height = 100, Height = 100,
Background = Brushes.Red, Background = Brushes.Red,
}), }),
new Border() new Border()
{ {
Name = "b2",
Width = 100, Width = 100,
Height = 100, Height = 100,
Background = Brushes.Red, Background = Brushes.Red,
@ -367,12 +369,14 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
{ {
(item1 = new Border() (item1 = new Border()
{ {
Name = "b3",
Width = 100, Width = 100,
Height = 100, Height = 100,
Background = Brushes.Red, Background = Brushes.Red,
}), }),
(item2 = new Border() (item2 = new Border()
{ {
Name = "b4",
Width = 100, Width = 100,
Height = 100, Height = 100,
Background = Brushes.Red, Background = Brushes.Red,
@ -400,19 +404,15 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
scroll.Offset = new Vector(0, 100); scroll.Offset = new Vector(0, 100);
//we don't have setup LayoutManager so we will make it manually // We don't have LayoutManager set up so do the layout pass manually.
scroll.Parent.InvalidateArrange(); scroll.Parent.InvalidateArrange();
container.InvalidateArrange(); container.InvalidateArrange();
container.Arrange(new Rect(container.DesiredSize)); container.Arrange(new Rect(container.DesiredSize));
result = container.GetVisualsAt(new Point(50, 150)).First(); result = container.GetVisualsAt(new Point(50, 150)).First();
Assert.Equal(item2, result); Assert.Equal(item2, result);
result = container.GetVisualsAt(new Point(50, 50)).First(); result = container.GetVisualsAt(new Point(50, 50)).First();
Assert.NotEqual(item1, result);
Assert.Equal(target, result); Assert.Equal(target, result);
} }
} }

Loading…
Cancel
Save