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>
/// <param name="action">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)
{
_jobRunner?.Post(action, priority);

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

@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
using System.Linq;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
@ -16,6 +15,7 @@ namespace Avalonia.Rendering
private readonly IRenderRoot _root;
private Scene _scene;
private IRenderTarget _renderTarget;
private List<IVisual> _dirty = new List<IVisual>();
private bool _needsUpdate;
private bool _needsRender;
@ -39,11 +39,14 @@ namespace Avalonia.Rendering
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;
Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render);
_dirty.Add(visual);
}
_needsUpdate = true;
}
public void Dispose()
@ -63,31 +66,6 @@ namespace Avalonia.Rendering
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)
@ -122,7 +100,22 @@ namespace Avalonia.Rendering
{
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;
_needsRender = true;
_root.Invalidate(new Rect(_root.ClientSize));
@ -130,10 +123,39 @@ namespace Avalonia.Rendering
private void OnRenderLoopTick(object sender, EventArgs e)
{
//if (_needsRender)
//{
// _needsRender = false;
//}
if (_needsUpdate)
{
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.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Media;
using Avalonia.Platform;
@ -25,30 +26,39 @@ namespace Avalonia.Rendering.SceneGraph
}
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)
{
var next = NextNodeAs<VisualNode>();
if (next == null || next != visualNode)
if (next == null || next != node)
{
Add(visualNode);
Add(node);
}
else
{
++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)
@ -196,17 +206,7 @@ namespace Avalonia.Rendering.SceneGraph
return Index < Node.Children.Count ? Node.Children[Index] as T : null;
}
private void Pop()
{
var frame = _stack.Pop();
var children = frame.Node.Children;
var index = frame.Index;
if (children.Count > index)
{
children.RemoveRange(index, children.Count - index);
}
}
private void Pop() => _stack.Pop();
class Frame
{

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

@ -10,8 +10,10 @@ namespace Avalonia.Rendering.SceneGraph
public interface IVisualNode : ISceneNode
{
IVisual Visual { get; }
Rect ClipBounds { get; set; }
bool ClipToBounds { get; set; }
IVisualNode Parent { get; }
Matrix Transform { get; }
Rect ClipBounds { get; }
bool ClipToBounds { get; }
IReadOnlyList<ISceneNode> Children { get; }
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;
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)
@ -28,6 +29,8 @@ namespace Avalonia.Rendering.SceneGraph
public void Add(IVisualNode node)
{
Contract.Requires<ArgumentNullException>(node != null);
_index.Add(node.Visual, node);
}
@ -51,9 +54,16 @@ namespace Avalonia.Rendering.SceneGraph
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);
@ -99,8 +109,6 @@ namespace Avalonia.Rendering.SceneGraph
}
}
dynamic d = node.Visual;
if (node.HitTest(p))
{
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 Scene Update(Scene scene)
public static void UpdateAll(Scene scene)
{
Dispatcher.UIThread.VerifyAccess();
scene = scene.Clone();
using (var impl = new DeferredDrawingContextImpl())
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 clipToBounds = visual.ClipToBounds;
var bounds = new Rect(visual.Bounds.Size);
var node = (VisualNode)scene.FindNode(visual) ?? CreateNode(visual, scene, parent);
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
contextImpl.AddChild(node);
if (visual.IsVisible && opacity > 0)
if (visual.IsVisible)
{
var m = Matrix.CreateTranslation(visual.Bounds.Position);
@ -55,6 +136,9 @@ namespace Avalonia.Rendering.SceneGraph
using (context.PushPostTransform(m))
using (context.PushTransformContainer())
{
forceRecurse = forceRecurse ||
node.Transform != contextImpl.Transform;
node.Transform = contextImpl.Transform;
node.ClipBounds = bounds * node.Transform;
node.ClipToBounds = clipToBounds;
@ -64,19 +148,42 @@ namespace Avalonia.Rendering.SceneGraph
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);
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 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;
Parent = parent;
Children = new List<ISceneNode>();
}
public IVisual Visual { get; }
public IVisualNode Parent { get; }
public Matrix Transform { get; set; }
public Rect ClipBounds { get; set; }
public bool ClipToBounds { get; set; }
@ -24,12 +32,21 @@ namespace Avalonia.Rendering.SceneGraph
public double Opacity { get; set; }
public IBrush OpacityMask { get; set; }
public List<ISceneNode> Children { get; }
public bool SubTreeUpdated { get; set; }
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)

19
src/Avalonia.Visuals/Visual.cs

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

11
src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs

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

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

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

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

@ -6,6 +6,7 @@ using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
using Avalonia.Layout;
namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
{
@ -35,10 +36,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
tree.Measure(Size.Infinity);
tree.Arrange(new Rect(tree.DesiredSize));
var initial = new Scene(tree);
var result = SceneBuilder.Update(initial);
var result = new Scene(tree);
SceneBuilder.UpdateAll(result);
Assert.NotSame(initial, result);
Assert.Equal(1, result.Root.Children.Count);
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]
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 expected = new IVisual[] { back, front };
@ -116,7 +152,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
tree.Measure(Size.Infinity);
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);
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.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 initialTextNode = initial.FindNode(textBlock).Children[0];
@ -156,9 +196,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Assert.NotNull(initialTextNode);
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];
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]
public void Should_Update_When_Control_Removed()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border;
TextBlock textBlock;
Decorator decorator;
Canvas canvas;
var tree = new TestRoot
{
Width = 100,
@ -188,9 +280,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
Child = border = new Border
{
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.Arrange(new Rect(tree.DesiredSize));
var initial = SceneBuilder.Update(new Scene(tree));
var initial = new Scene(tree);
SceneBuilder.UpdateAll(initial);
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];
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.Linq;
using Avalonia.Controls;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Visuals.UnitTests
@ -73,8 +75,19 @@ namespace Avalonia.Visuals.UnitTests
var called1 = false;
var called2 = false;
child1.AttachedToVisualTree += (s, e) => called1 = true;
child2.AttachedToVisualTree += (s, e) => called2 = true;
child1.AttachedToVisualTree += (s, e) =>
{
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;
@ -92,14 +105,83 @@ namespace Avalonia.Visuals.UnitTests
var called2 = false;
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;
Assert.True(called1);
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]
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()
{
Name = "b1",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
new Border()
{
Name = "b2",
Width = 100,
Height = 100,
Background = Brushes.Red,
@ -367,12 +369,14 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
{
(item1 = new Border()
{
Name = "b3",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
(item2 = new Border()
{
Name = "b4",
Width = 100,
Height = 100,
Background = Brushes.Red,
@ -400,19 +404,15 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
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();
container.InvalidateArrange();
container.Arrange(new Rect(container.DesiredSize));
result = container.GetVisualsAt(new Point(50, 150)).First();
Assert.Equal(item2, result);
result = container.GetVisualsAt(new Point(50, 50)).First();
Assert.NotEqual(item1, result);
Assert.Equal(target, result);
}
}

Loading…
Cancel
Save