diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index aee6a02e73..671032385c 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -79,6 +79,8 @@ namespace Avalonia.Threading /// /// The method. /// The priority with which to invoke the method. + // 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); diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index fd133809c5..0ad0599b18 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/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 _dirty = new List(); 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; + } + } } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 5717ccdf0e..7e4dee81a4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/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(); - 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 { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 7850100804..a402af100d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/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 Children { get; } bool HitTest(Point p); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 12a9a45f43..47804d95e6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -12,8 +12,9 @@ namespace Avalonia.Rendering.SceneGraph private Dictionary _index; public Scene(IVisual rootVisual) - : this(new VisualNode(rootVisual), new Dictionary()) + : this(new VisualNode(rootVisual, null), new Dictionary()) { + _index.Add(rootVisual, Root); } internal Scene(VisualNode root, Dictionary index) @@ -28,6 +29,8 @@ namespace Avalonia.Rendering.SceneGraph public void Add(IVisualNode node) { + Contract.Requires(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 index) + public void Remove(IVisualNode node) + { + Contract.Requires(node != null); + + _index.Remove(node.Visual); + } + + private VisualNode Clone(VisualNode source, IVisualNode parent, Dictionary 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; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 4749c99925..46ea25626f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/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); + } + } + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 98d7ceab57..1dbf30a7b9 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/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(); + if (parent == null && visual.VisualParent != null) + { + throw new AvaloniaInternalException( + "Attempted to create root VisualNode for parented visual."); + } + Visual = visual; + Parent = parent; + Children = new List(); } 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 Children { get; } + public bool SubTreeUpdated { get; set; } IReadOnlyList 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) diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index aca910b6e8..675ad042e1 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -88,6 +88,7 @@ namespace Avalonia AvaloniaProperty.Register(nameof(ZIndex)); private Rect _bounds; + private IRenderRoot _visualRoot; private IVisual _visualParent; /// @@ -233,11 +234,7 @@ namespace Avalonia /// /// Gets the root of the visual tree, if the control is attached to a visual tree. /// - protected IRenderRoot VisualRoot - { - get; - private set; - } + protected IRenderRoot VisualRoot => _visualRoot ?? (this as IRenderRoot); /// /// 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().FirstOrDefault(); - var e = new VisualTreeAttachmentEventArgs(root); + var e = new VisualTreeAttachmentEventArgs(_visualParent, root); OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs b/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs index 7a3b97f55b..8c6e660d10 100644 --- a/src/Avalonia.Visuals/VisualTreeAttachmentEventArgs.cs +++ b/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 /// /// Initializes a new instance of the class. /// + /// The parent that the visual is being attached to or detached from. /// The root visual. - public VisualTreeAttachmentEventArgs(IRenderRoot root) + public VisualTreeAttachmentEventArgs(IVisual parent, IRenderRoot root) { + Contract.Requires(parent != null); Contract.Requires(root != null); + Parent = parent; Root = root; } + /// + /// Gets the parent that the visual is being attached to or detached from. + /// + public IVisual Parent { get; } + /// /// Gets the root of the visual tree that the visual is being attached to or detached from. /// diff --git a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject index 7c25da7d15..0c8a7ff2ac 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject +++ b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject @@ -7,7 +7,7 @@ true false false - true + false false false true @@ -23,5 +23,8 @@ AutoDetect STA x86 + + + AbnormalReferenceResolution \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 97630d5632..f52efc10bb 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/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(); + 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())); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs index 9cec9e434b..28f0a7ad13 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs +++ b/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(); + + 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(); + + 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() { diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs index 4dbec609f8..44cc291be9 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs +++ b/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); } }