Browse Source

Merge branch 'master' into issues/855_test

Jeremy Koritzinsky 8 years ago
parent
commit
4df484d047

+ 1 - 2
.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject

@@ -1,7 +1,6 @@
 <ProjectConfiguration>
   <Settings>
-    <DefaultTestTimeout>1000</DefaultTestTimeout>
-    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+    <DefaultTestTimeout>3000</DefaultTestTimeout>
     <PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
   </Settings>
 </ProjectConfiguration>

+ 13 - 0
src/Avalonia.Base/AvaloniaObject.cs

@@ -240,6 +240,19 @@ namespace Avalonia
             return (T)GetValue((AvaloniaProperty)property);
         }
 
+        /// <summary>
+        /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <returns>True if the property is animating, otherwise false.</returns>
+        public bool IsAnimating(AvaloniaProperty property)
+        {
+            Contract.Requires<ArgumentNullException>(property != null);
+            VerifyAccess();
+
+            return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false;
+        }
+
         /// <summary>
         /// Checks whether a <see cref="AvaloniaProperty"/> is set on this object.
         /// </summary>

+ 7 - 0
src/Avalonia.Base/IAvaloniaObject.cs

@@ -31,6 +31,13 @@ namespace Avalonia
         /// <returns>The value.</returns>
         T GetValue<T>(AvaloniaProperty<T> property);
 
+        /// <summary>
+        /// Checks whether a <see cref="AvaloniaProperty"/> is animating.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <returns>True if the property is animating, otherwise false.</returns>
+        bool IsAnimating(AvaloniaProperty property);
+
         /// <summary>
         /// Checks whether a <see cref="AvaloniaProperty"/> is set on this object.
         /// </summary>

+ 12 - 0
src/Avalonia.Base/PriorityValue.cs

@@ -53,6 +53,18 @@ namespace Avalonia
             _validate = validate;
         }
 
+        /// <summary>
+        /// Gets a value indicating whether the property is animating.
+        /// </summary>
+        public bool IsAnimating
+        {
+            get
+            {
+                return ValuePriority <= (int)BindingPriority.Animation && 
+                    GetLevel(ValuePriority).ActiveBindingIndex != -1;
+            }
+        }
+
         /// <summary>
         /// Gets the owner of the value.
         /// </summary>

+ 30 - 20
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -25,11 +25,9 @@ namespace Avalonia.Rendering
         private readonly IRenderLoop _renderLoop;
         private readonly IVisual _root;
         private readonly ISceneBuilder _sceneBuilder;
-        private readonly RenderLayers _layers;
 
         private bool _running;
         private Scene _scene;
-        private IRenderTarget _renderTarget;
         private DirtyVisuals _dirty;
         private IRenderTargetBitmapImpl _overlay;
         private bool _updateQueued;
@@ -56,7 +54,7 @@ namespace Avalonia.Rendering
             _dispatcher = dispatcher ?? Dispatcher.UIThread;
             _root = root;
             _sceneBuilder = sceneBuilder ?? new SceneBuilder();
-            _layers = new RenderLayers();
+            Layers = new RenderLayers();
             _renderLoop = renderLoop;
         }
 
@@ -78,9 +76,9 @@ namespace Avalonia.Rendering
             Contract.Requires<ArgumentNullException>(renderTarget != null);
 
             _root = root;
-            _renderTarget = renderTarget;
+            RenderTarget = renderTarget;
             _sceneBuilder = sceneBuilder ?? new SceneBuilder();
-            _layers = new RenderLayers();
+            Layers = new RenderLayers();
         }
 
         /// <inheritdoc/>
@@ -94,6 +92,16 @@ namespace Avalonia.Rendering
         /// </summary>
         public string DebugFramesPath { get; set; }
 
+        /// <summary>
+        /// Gets the render layers.
+        /// </summary>
+        internal RenderLayers Layers { get; }
+
+        /// <summary>
+        /// Gets the current render target.
+        /// </summary>
+        internal IRenderTarget RenderTarget { get; private set; }
+
         /// <inheritdoc/>
         public void AddDirty(IVisual visual)
         {
@@ -173,9 +181,9 @@ namespace Avalonia.Rendering
             bool renderOverlay = DrawDirtyRects || DrawFps;
             bool composite = false;
 
-            if (_renderTarget == null)
+            if (RenderTarget == null)
             {
-                _renderTarget = ((IRenderRoot)_root).CreateRenderTarget();
+                RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
             }
 
             if (renderOverlay)
@@ -191,8 +199,8 @@ namespace Avalonia.Rendering
 
                     if (scene.Generation != _lastSceneId)
                     {
-                        context = _renderTarget.CreateDrawingContext(this);
-                        _layers.Update(scene, context);
+                        context = RenderTarget.CreateDrawingContext(this);
+                        Layers.Update(scene, context);
 
                         RenderToLayers(scene);
 
@@ -208,13 +216,13 @@ namespace Avalonia.Rendering
 
                     if (renderOverlay)
                     {
-                        context = context ?? _renderTarget.CreateDrawingContext(this);
+                        context = context ?? RenderTarget.CreateDrawingContext(this);
                         RenderOverlay(scene, context);
                         RenderComposite(scene, context);
                     }
                     else if (composite)
                     {
-                        context = context ?? _renderTarget.CreateDrawingContext(this);
+                        context = context ?? RenderTarget.CreateDrawingContext(this);
                         RenderComposite(scene, context);
                     }
 
@@ -224,8 +232,8 @@ namespace Avalonia.Rendering
             catch (RenderTargetCorruptedException ex)
             {
                 Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
-                _renderTarget?.Dispose();
-                _renderTarget = null;
+                RenderTarget?.Dispose();
+                RenderTarget = null;
             }
         }
 
@@ -235,9 +243,11 @@ namespace Avalonia.Rendering
             {
                 clipBounds = node.ClipBounds.Intersect(clipBounds);
 
-                if (!clipBounds.IsEmpty)
+                if (!clipBounds.IsEmpty && node.Opacity > 0)
                 {
-                    node.BeginRender(context);
+                    var isLayerRoot = node.Visual == layer;
+
+                    node.BeginRender(context, isLayerRoot);
 
                     foreach (var operation in node.DrawOperations)
                     {
@@ -251,7 +261,7 @@ namespace Avalonia.Rendering
                         Render(context, (VisualNode)child, layer, clipBounds);
                     }
 
-                    node.EndRender(context);
+                    node.EndRender(context, isLayerRoot);
                 }
             }
         }
@@ -262,7 +272,7 @@ namespace Avalonia.Rendering
             {
                 foreach (var layer in scene.Layers)
                 {
-                    var renderTarget = _layers[layer.LayerRoot].Bitmap;
+                    var renderTarget = Layers[layer.LayerRoot].Bitmap;
                     var node = (VisualNode)scene.FindNode(layer.LayerRoot);
 
                     if (node != null)
@@ -322,7 +332,7 @@ namespace Avalonia.Rendering
 
             foreach (var layer in scene.Layers)
             {
-                var bitmap = _layers[layer.LayerRoot].Bitmap;
+                var bitmap = Layers[layer.LayerRoot].Bitmap;
                 var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
 
                 if (layer.GeometryClip != null)
@@ -353,7 +363,7 @@ namespace Avalonia.Rendering
 
             if (DrawFps)
             {
-                RenderFps(context, clientRect, true);
+                RenderFps(context, clientRect, scene.Layers.Count);
             }
         }
 
@@ -442,7 +452,7 @@ namespace Avalonia.Rendering
         {
             var index = 0;
 
-            foreach (var layer in _layers)
+            foreach (var layer in Layers)
             {
                 var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png");
                 layer.Bitmap.Save(fileName);

+ 1 - 1
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -69,7 +69,7 @@ namespace Avalonia.Rendering
 
                     if (DrawFps)
                     {
-                        RenderFps(context.PlatformImpl, _root.Bounds, true);
+                        RenderFps(context.PlatformImpl, _root.Bounds, null);
                     }
                 }
             }

+ 11 - 6
src/Avalonia.Visuals/Rendering/RendererBase.cs

@@ -22,15 +22,12 @@ namespace Avalonia.Rendering
             };
         }
 
-        protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount)
+        protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount)
         {
             var now = _stopwatch.Elapsed;
             var elapsed = now - _lastFpsUpdate;
 
-            if (incrementFrameCount)
-            {
-                ++_framesThisSecond;
-            }
+            ++_framesThisSecond;
 
             if (elapsed.TotalSeconds > 1)
             {
@@ -39,7 +36,15 @@ namespace Avalonia.Rendering
                 _lastFpsUpdate = now;
             }
 
-            _fpsText.Text = string.Format("FPS: {0:000}", _fps);
+            if (layerCount.HasValue)
+            {
+                _fpsText.Text = string.Format("Layers: {0} FPS: {1:000}", layerCount, _fps);
+            }
+            else
+            {
+                _fpsText.Text = string.Format("FPS: {0:000}", _fps);
+            }
+
             var size = _fpsText.Measure();
             var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height);
 

+ 4 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@@ -72,13 +72,15 @@ namespace Avalonia.Rendering.SceneGraph
         /// Sets up the drawing context for rendering the node's geometry.
         /// </summary>
         /// <param name="context">The drawing context.</param>
-        void BeginRender(IDrawingContextImpl context);
+        /// <param name="skipOpacity">Whether to skip pushing the control's opacity.</param>
+        void BeginRender(IDrawingContextImpl context, bool skipOpacity);
 
         /// <summary>
         /// Resets the drawing context after rendering the node's geometry.
         /// </summary>
         /// <param name="context">The drawing context.</param>
-        void EndRender(IDrawingContextImpl context);
+        /// <param name="skipOpacity">Whether to skip popping the control's opacity.</param>
+        void EndRender(IDrawingContextImpl context, bool skipOpacity);
 
         /// <summary>
         /// Hit test the geometry in this node.

+ 12 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -167,7 +167,6 @@ namespace Avalonia.Rendering.SceneGraph
                 using (context.PushPostTransform(m))
                 using (context.PushTransformContainer())
                 {
-                    var startLayer = opacity < 1 || visual.OpacityMask != null;
                     var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip);
 
                     forceRecurse = forceRecurse ||
@@ -179,9 +178,11 @@ namespace Avalonia.Rendering.SceneGraph
                     node.ClipToBounds = clipToBounds;
                     node.GeometryClip = visual.Clip?.PlatformImpl;
                     node.Opacity = opacity;
-                    node.OpacityMask = visual.OpacityMask;
 
-                    if (startLayer)
+                    // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning.
+                    node.OpacityMask = visual.OpacityMask?.ToImmutable();
+
+                    if (ShouldStartLayer(visual))
                     {
                         if (node.LayerRoot != visual)
                         {
@@ -366,6 +367,14 @@ namespace Avalonia.Rendering.SceneGraph
             }
         }
 
+        private static bool ShouldStartLayer(IVisual visual)
+        {
+            var o = visual as IAvaloniaObject;
+            return visual.VisualChildren.Count > 0 &&
+                o != null &&
+                o.IsAnimating(Visual.OpacityProperty);
+        }
+
         private static IGeometryImpl CreateLayerGeometryClip(VisualNode node)
         {
             IGeometryImpl result = null;

+ 28 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -22,6 +22,7 @@ namespace Avalonia.Rendering.SceneGraph
         private List<IVisualNode> _children;
         private List<IDrawOperation> _drawOperations;
         private bool _drawOperationsCloned;
+        private Matrix transformRestore;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="VisualNode"/> class.
@@ -218,8 +219,10 @@ namespace Avalonia.Rendering.SceneGraph
         }
 
         /// <inheritdoc/>
-        public void BeginRender(IDrawingContextImpl context)
+        public void BeginRender(IDrawingContextImpl context, bool skipOpacity)
         {
+            transformRestore = context.Transform;
+
             if (ClipToBounds)
             {
                 context.Transform = Matrix.Identity;
@@ -228,24 +231,47 @@ namespace Avalonia.Rendering.SceneGraph
 
             context.Transform = Transform;
 
+            if (Opacity != 1 && !skipOpacity)
+            {
+                context.PushOpacity(Opacity);
+            }
+
             if (GeometryClip != null)
             {
                 context.PushGeometryClip(GeometryClip);
             }
+
+            if (OpacityMask != null)
+            {
+                context.PushOpacityMask(OpacityMask, ClipBounds);
+            }
         }
 
         /// <inheritdoc/>
-        public void EndRender(IDrawingContextImpl context)
+        public void EndRender(IDrawingContextImpl context, bool skipOpacity)
         {
+            if (OpacityMask != null)
+            {
+                context.PopOpacityMask();
+            }
+
             if (GeometryClip != null)
             {
                 context.PopGeometryClip();
             }
 
+            if (Opacity != 1 && !skipOpacity)
+            {
+                context.PopOpacity();
+            }
+
             if (ClipToBounds)
             {
+                context.Transform = Matrix.Identity;
                 context.PopClip();
             }
+
+            context.Transform = transformRestore;
         }
 
         private Rect CalculateBounds()

+ 40 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -418,6 +418,46 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal(expected, child.DoubleValue);
         }
 
+        [Fact]
+        public void IsAnimating_On_Property_With_No_Value_Returns_False()
+        {
+            var target = new Class1();
+
+            Assert.False(target.IsAnimating(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void IsAnimating_On_Property_With_Animation_Value_Returns_False()
+        {
+            var target = new Class1();
+
+            target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation);
+
+            Assert.False(target.IsAnimating(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void IsAnimating_On_Property_With_Non_Animation_Binding_Returns_False()
+        {
+            var target = new Class1();
+            var source = new Subject<string>();
+
+            target.Bind(Class1.FooProperty, source, BindingPriority.LocalValue);
+
+            Assert.False(target.IsAnimating(Class1.FooProperty));
+        }
+
+        [Fact]
+        public void IsAnimating_On_Property_With_Animation_Binding_Returns_True()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<string>("foo");
+
+            target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
+
+            Assert.True(target.IsAnimating(Class1.FooProperty));
+        }
+
         /// <summary>
         /// Returns an observable that returns a single value but does not complete.
         /// </summary>

+ 12 - 0
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Reactive.Subjects;
 using Avalonia.Data;
 using Xunit;
 
@@ -70,6 +71,17 @@ namespace Avalonia.Base.UnitTests
             Assert.Same(p1.Initialized, p2.Initialized);
         }
 
+        [Fact]
+        public void IsAnimating_On_DirectProperty_With_Binding_Returns_False()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<string>("foo");
+
+            target.Bind(Class1.FooProperty, source, BindingPriority.Animation);
+
+            Assert.False(target.IsAnimating(Class1.FooProperty));
+        }
+
         private class Class1 : AvaloniaObject
         {
             public static readonly DirectProperty<Class1, string> FooProperty =

+ 5 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@@ -135,6 +135,11 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
+            public bool IsAnimating(AvaloniaProperty property)
+            {
+                throw new NotImplementedException();
+            }
+
             public bool IsSet(AvaloniaProperty property)
             {
                 throw new NotImplementedException();

+ 5 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@@ -165,6 +165,11 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
+            public bool IsAnimating(AvaloniaProperty property)
+            {
+                throw new NotImplementedException();
+            }
+
             public bool IsSet(AvaloniaProperty property)
             {
                 throw new NotImplementedException();

+ 5 - 0
tests/Avalonia.Styling.UnitTests/TestControlBase.cs

@@ -59,6 +59,11 @@ namespace Avalonia.Styling.UnitTests
             throw new NotImplementedException();
         }
 
+        public bool IsAnimating(AvaloniaProperty property)
+        {
+            throw new NotImplementedException();
+        }
+
         public bool IsSet(AvaloniaProperty property)
         {
             throw new NotImplementedException();

+ 5 - 0
tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs

@@ -67,6 +67,11 @@ namespace Avalonia.Styling.UnitTests
             throw new NotImplementedException();
         }
 
+        public bool IsAnimating(AvaloniaProperty property)
+        {
+            throw new NotImplementedException();
+        }
+
         public bool IsSet(AvaloniaProperty property)
         {
             throw new NotImplementedException();

+ 15 - 3
tests/Avalonia.UnitTests/TestRoot.cs

@@ -16,8 +16,6 @@ namespace Avalonia.UnitTests
     public class TestRoot : Decorator, IFocusScope, ILayoutRoot, IInputRoot, INameScope, IRenderRoot, IStyleRoot
     {
         private readonly NameScope _nameScope = new NameScope();
-        private readonly IRenderTarget _renderTarget = Mock.Of<IRenderTarget>(
-            x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()) == Mock.Of<IDrawingContextImpl>());
 
         public TestRoot()
         {
@@ -65,7 +63,21 @@ namespace Avalonia.UnitTests
 
         IStyleHost IStyleHost.StylingParent => StylingParent;
 
-        public IRenderTarget CreateRenderTarget() => _renderTarget;
+        public IRenderTarget CreateRenderTarget()
+        {
+            var dc = new Mock<IDrawingContextImpl>();
+            dc.Setup(x => x.CreateLayer(It.IsAny<Size>())).Returns(() =>
+            {
+                var layerDc = new Mock<IDrawingContextImpl>();
+                var layer = new Mock<IRenderTargetBitmapImpl>();
+                layer.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(layerDc.Object);
+                return layer.Object;
+            });
+
+            var result = new Mock<IRenderTarget>();
+            result.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(dc.Object);
+            return result.Object;
+        }
 
         public void Invalidate(Rect rect)
         {

+ 206 - 67
tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs

@@ -1,7 +1,9 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Reactive.Subjects;
 using Avalonia.Controls;
+using Avalonia.Data;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
@@ -19,28 +21,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering
         [Fact]
         public void First_Frame_Calls_UpdateScene_On_Dispatcher()
         {
-            var loop = new Mock<IRenderLoop>();
             var root = new TestRoot();
 
             var dispatcher = new Mock<IDispatcher>();
             dispatcher.Setup(x => x.InvokeAsync(It.IsAny<Action>(), DispatcherPriority.Render))
                 .Callback<Action, DispatcherPriority>((a, p) => a());
 
-            var target = new DeferredRenderer(
-                root,
-                loop.Object,
-                sceneBuilder: MockSceneBuilder(root).Object,
-                dispatcher: dispatcher.Object);
+            CreateTargetAndRunFrame(root, dispatcher: dispatcher.Object);
 
-            target.Start();
-            RunFrame(loop);
-
-#if !NETCOREAPP1_1 // Delegate.Method is not available in netcoreapp1.1
             dispatcher.Verify(x => 
                 x.InvokeAsync(
                     It.Is<Action>(a => a.Method.Name == "UpdateScene"),
                     DispatcherPriority.Render));
-#endif
         }
 
         [Fact]
@@ -49,15 +41,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering
             var loop = new Mock<IRenderLoop>();
             var root = new TestRoot();
             var sceneBuilder = MockSceneBuilder(root);
-            var dispatcher = new ImmediateDispatcher();
-            var target = new DeferredRenderer(
-                root,
-                loop.Object,
-                sceneBuilder: sceneBuilder.Object,
-                dispatcher: dispatcher);
 
-            target.Start();
-            RunFrame(loop);
+            CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object);
 
             sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>()));
         }
@@ -68,12 +53,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering
             var loop = new Mock<IRenderLoop>();
             var root = new TestRoot();
             var sceneBuilder = MockSceneBuilder(root);
-            var dispatcher = new ImmediateDispatcher();
             var target = new DeferredRenderer(
                 root,
                 loop.Object,
-                sceneBuilder: sceneBuilder.Object,
-                dispatcher: dispatcher);
+                sceneBuilder: sceneBuilder.Object);
 
             target.Start();
             IgnoreFirstFrame(loop, sceneBuilder);
@@ -127,12 +110,93 @@ namespace Avalonia.Visuals.UnitTests.Rendering
         }
 
         [Fact]
-        public void Frame_Should_Create_Layer_For_Root()
+        public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity()
+        {
+            var root = new TestRoot
+            {
+                Width = 100,
+                Height = 100,
+                Child = new Border
+                {
+                    Background = Brushes.Red,
+                    Opacity = 0.5,
+                }
+            };
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
+
+            var target = CreateTargetAndRunFrame(root);
+            var context = GetLayerContext(target, root);
+            var animation = new BehaviorSubject<double>(0.5);
+
+            context.Verify(x => x.PushOpacity(0.5), Times.Once);
+            context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+            context.Verify(x => x.PopOpacity(), Times.Once);
+        }
+
+        [Fact]
+        public void Should_Not_Draw_Controls_With_0_Opacity()
+        {
+            var root = new TestRoot
+            {
+                Width = 100,
+                Height = 100,
+                Child = new Border
+                {
+                    Background = Brushes.Red,
+                    Opacity = 0,
+                    Child = new Border
+                    {
+                        Background = Brushes.Green,
+                    }
+                }
+            };
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
+
+            var target = CreateTargetAndRunFrame(root);
+            var context = GetLayerContext(target, root);
+            var animation = new BehaviorSubject<double>(0.5);
+
+            context.Verify(x => x.PushOpacity(0.5), Times.Never);
+            context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Never);
+            context.Verify(x => x.PopOpacity(), Times.Never);
+        }
+
+        [Fact]
+        public void Should_Push_Opacity_Mask()
+        {
+            var root = new TestRoot
+            {
+                Width = 100,
+                Height = 100,
+                Child = new Border
+                {
+                    Background = Brushes.Red,
+                    OpacityMask = Brushes.Green,
+                }
+            };
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
+
+            var target = CreateTargetAndRunFrame(root);
+            var context = GetLayerContext(target, root);
+            var animation = new BehaviorSubject<double>(0.5);
+
+            context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once);
+            context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+            context.Verify(x => x.PopOpacityMask(), Times.Once);
+        }
+
+        [Fact]
+        public void Should_Create_Layer_For_Root()
         {
             var loop = new Mock<IRenderLoop>();
             var root = new TestRoot();
             var rootLayer = new Mock<IRenderTargetBitmapImpl>();
-            var dispatcher = new ImmediateDispatcher();
 
             var sceneBuilder = new Mock<ISceneBuilder>();
             sceneBuilder.Setup(x => x.UpdateAll(It.IsAny<Scene>()))
@@ -143,23 +207,53 @@ namespace Avalonia.Visuals.UnitTests.Rendering
                 });
 
             var renderInterface = new Mock<IPlatformRenderInterface>();
+            var target = CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object);
 
-            var target = new DeferredRenderer(
-                root,
-                loop.Object,
-                sceneBuilder: sceneBuilder.Object,
-                //layerFactory: layers.Object,
-                dispatcher: dispatcher);
+            Assert.Single(target.Layers);
+        }
 
-            target.Start();
+        [Fact]
+        public void Should_Create_And_Delete_Layers_For_Controls_With_Animated_Opacity()
+        {
+            Border border;
+            var root = new TestRoot
+            {
+                Width = 100,
+                Height = 100,
+                Child = new Border
+                {
+                    Background = Brushes.Red,
+                    Child = border = new Border
+                    {
+                        Background = Brushes.Green,
+                        Child = new Canvas(),
+                        Opacity = 0.9,
+                    }
+                }
+            };
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
+
+            var loop = new Mock<IRenderLoop>();
+            var target = CreateTargetAndRunFrame(root, loop: loop);
+
+            Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
+
+            var animation = new BehaviorSubject<double>(0.5);
+            border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
             RunFrame(loop);
 
-            var context = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null));
-            context.Verify(x => x.CreateLayer(root.ClientSize));
+            Assert.Equal(new IVisual[] { root, border }, target.Layers.Select(x => x.LayerRoot));
+
+            animation.OnCompleted();
+            RunFrame(loop);
+
+            Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
         }
 
         [Fact]
-        public void Should_Create_And_Delete_Layers_For_Transparent_Controls()
+        public void Should_Not_Create_Layer_For_Childless_Control_With_Animated_Opacity()
         {
             Border border;
             var root = new TestRoot
@@ -176,51 +270,96 @@ namespace Avalonia.Visuals.UnitTests.Rendering
                 }
             };
 
+            var animation = new BehaviorSubject<double>(0.5);
+            border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+
             root.Measure(Size.Infinity);
             root.Arrange(new Rect(root.DesiredSize));
 
-            var rootLayer = CreateLayer();
-            var borderLayer = CreateLayer();
-            var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null));
-            renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny<Size>()))
-                .Returns(rootLayer)
-                .Returns(borderLayer);
-
             var loop = new Mock<IRenderLoop>();
-            var target = new DeferredRenderer(
-                root,
-                loop.Object,
-                dispatcher: new ImmediateDispatcher());
-            root.Renderer = target;
+            var target = CreateTargetAndRunFrame(root, loop: loop);
 
-            target.Start();
-            RunFrame(loop);
+            Assert.Single(target.Layers);
+        }
+
+        [Fact]
+        public void Should_Not_Push_Opacity_For_Transparent_Layer_Root_Control()
+        {
+            Border border;
+            var root = new TestRoot
+            {
+                Width = 100,
+                Height = 100,
+                Child = border = new Border
+                {
+                    Background = Brushes.Red,
+                    Child = new Canvas(),
+                }
+            };
+
+            var animation = new BehaviorSubject<double>(0.5);
+            border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
 
-            var rootContext = Mock.Get(rootLayer.CreateDrawingContext(null));
-            var borderContext = Mock.Get(borderLayer.CreateDrawingContext(null));
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
 
-            rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
-            rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
-            borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never);
+            var target = CreateTargetAndRunFrame(root);
+            var context = GetLayerContext(target, border);
 
-            rootContext.ResetCalls();
-            borderContext.ResetCalls();
-            border.Opacity = 0.5;
-            RunFrame(loop);
+            context.Verify(x => x.PushOpacity(0.5), Times.Never);
+            context.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
+            context.Verify(x => x.PopOpacity(), Times.Never);
+        }
+
+        [Fact]
+        public void Should_Draw_Transparent_Layer_With_Correct_Opacity()
+        {
+            Border border;
+            var root = new TestRoot
+            {
+                Width = 100,
+                Height = 100,
+                Child = border = new Border
+                {
+                    Background = Brushes.Red,
+                    Child = new Canvas(),
+                }
+            };
+
+            var animation = new BehaviorSubject<double>(0.5);
+            border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
 
-            rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
-            rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Never);
-            borderContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
+            var target = CreateTargetAndRunFrame(root);
+            var context = Mock.Get(target.RenderTarget.CreateDrawingContext(null));
+            var borderLayer = target.Layers[border].Bitmap;
 
-            rootContext.ResetCalls();
-            borderContext.ResetCalls();
-            border.Opacity = 1;
+            context.Verify(x => x.DrawImage(borderLayer, 0.5, It.IsAny<Rect>(), It.IsAny<Rect>()));
+        }
+
+        private DeferredRenderer CreateTargetAndRunFrame(
+            TestRoot root,
+            Mock<IRenderLoop> loop = null,
+            ISceneBuilder sceneBuilder = null,
+            IDispatcher dispatcher = null)
+        {
+            loop = loop ?? new Mock<IRenderLoop>();
+            var target = new DeferredRenderer(
+                root,
+                loop.Object,
+                sceneBuilder: sceneBuilder,
+                dispatcher: dispatcher ?? new ImmediateDispatcher());
+            root.Renderer = target;
+            target.Start();
             RunFrame(loop);
+            return target;
+        }
 
-            Mock.Get(borderLayer).Verify(x => x.Dispose());
-            rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once);
-            rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once);
-            borderContext.Verify(x => x.FillRectangle(It.IsAny<IBrush>(), It.IsAny<Rect>(), It.IsAny<float>()), Times.Never);
+        private Mock<IDrawingContextImpl> GetLayerContext(DeferredRenderer renderer, IControl layerRoot)
+        {
+            return Mock.Get(renderer.Layers[layerRoot].Bitmap.CreateDrawingContext(null));
         }
 
         private void IgnoreFirstFrame(Mock<IRenderLoop> loop, Mock<ISceneBuilder> sceneBuilder)

+ 5 - 1
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -9,6 +9,8 @@ using Xunit;
 using Avalonia.Layout;
 using Moq;
 using Avalonia.Platform;
+using System.Reactive.Subjects;
+using Avalonia.Data;
 
 namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
 {
@@ -620,13 +622,15 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                         Margin = new Thickness(0, 10, 0, 0),
                         Child = border = new Border
                         {
-                            Opacity = 0.5,
                             Background = Brushes.Red,
                             Child = canvas = new Canvas(),
                         }
                     }
                 };
 
+                var animation = new BehaviorSubject<double>(0.5);
+                border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+
                 var scene = new Scene(tree);
                 var sceneBuilder = new SceneBuilder();
                 sceneBuilder.UpdateAll(scene);

+ 29 - 46
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs

@@ -7,14 +7,15 @@ using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
 using Avalonia.Layout;
-using Avalonia.Rendering;
+using System.Reactive.Subjects;
+using Avalonia.Data;
 
 namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
 {
     public partial class SceneBuilderTests
     {
         [Fact]
-        public void Control_With_Transparency_Should_Start_New_Layer()
+        public void Control_With_Animated_Opacity_And_Children_Should_Start_New_Layer()
         {
             using (TestApplication())
             {
@@ -31,10 +32,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                         Padding = new Thickness(11),
                         Child = border = new Border
                         {
-                            Opacity = 0.5,
                             Background = Brushes.Red,
                             Padding = new Thickness(12),
-                            Child = canvas = new Canvas(),
+                            Child = canvas = new Canvas()
                         }
                     }
                 };
@@ -42,6 +42,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
                 layout.ExecuteInitialLayoutPass(tree);
 
+                var animation = new BehaviorSubject<double>(0.5);
+                border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+
                 var scene = new Scene(tree);
                 var sceneBuilder = new SceneBuilder();
                 sceneBuilder.UpdateAll(scene);
@@ -58,7 +61,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 Assert.Equal(2, scene.Layers.Count());
                 Assert.Empty(scene.Layers.Select(x => x.LayerRoot).Except(new IVisual[] { tree, border }));
 
-                border.Opacity = 1;
+                animation.OnCompleted();
                 scene = scene.Clone();
 
                 sceneBuilder.Update(scene, border);
@@ -80,13 +83,12 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
         }
 
         [Fact]
-        public void Control_With_OpacityMask_Should_Start_New_Layer()
+        public void Control_With_Animated_Opacity_And_No_Children_Should_Not_Start_New_Layer()
         {
             using (TestApplication())
             {
                 Decorator decorator;
                 Border border;
-                Canvas canvas;
                 var tree = new TestRoot
                 {
                     Padding = new Thickness(10),
@@ -97,10 +99,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                         Padding = new Thickness(11),
                         Child = border = new Border
                         {
-                            OpacityMask = Brushes.Red,
                             Background = Brushes.Red,
-                            Padding = new Thickness(12),
-                            Child = canvas = new Canvas(),
                         }
                     }
                 };
@@ -108,45 +107,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
                 layout.ExecuteInitialLayoutPass(tree);
 
+                var animation = new BehaviorSubject<double>(0.5);
+                border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+
                 var scene = new Scene(tree);
                 var sceneBuilder = new SceneBuilder();
                 sceneBuilder.UpdateAll(scene);
 
-                var rootNode = (VisualNode)scene.Root;
-                var borderNode = (VisualNode)scene.FindNode(border);
-                var canvasNode = (VisualNode)scene.FindNode(canvas);
-
-                Assert.Same(tree, rootNode.LayerRoot);
-                Assert.Same(border, borderNode.LayerRoot);
-                Assert.Same(border, canvasNode.LayerRoot);
-                Assert.Equal(Brushes.Red, scene.Layers[border].OpacityMask);
-
-                Assert.Equal(2, scene.Layers.Count());
-                Assert.Empty(scene.Layers.Select(x => x.LayerRoot).Except(new IVisual[] { tree, border }));
-
-                border.OpacityMask = null;
-                scene = scene.Clone();
-
-                sceneBuilder.Update(scene, border);
-
-                rootNode = (VisualNode)scene.Root;
-                borderNode = (VisualNode)scene.FindNode(border);
-                canvasNode = (VisualNode)scene.FindNode(canvas);
-
-                Assert.Same(tree, rootNode.LayerRoot);
-                Assert.Same(tree, borderNode.LayerRoot);
-                Assert.Same(tree, canvasNode.LayerRoot);
                 Assert.Single(scene.Layers);
-
-                var rootDirty = scene.Layers[tree].Dirty;
-
-                Assert.Single(rootDirty);
-                Assert.Equal(new Rect(21, 21, 58, 78), rootDirty.Single());
             }
         }
 
         [Fact]
-        public void Removing_Transparent_Control_Should_Remove_Layers()
+        public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers()
         {
             using (TestApplication())
             {
@@ -163,13 +136,12 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                         Padding = new Thickness(11),
                         Child = border = new Border
                         {
-                            Opacity = 0.5,
                             Background = Brushes.Red,
                             Padding = new Thickness(12),
                             Child = canvas = new Canvas
                             {
-                                Opacity = 0.75,
-                            },
+                                Children = { new TextBlock() },
+                            }
                         }
                     }
                 };
@@ -177,6 +149,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
                 layout.ExecuteInitialLayoutPass(tree);
 
+                var animation = new BehaviorSubject<double>(0.5);
+                border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+                canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation);
+
                 var scene = new Scene(tree);
                 var sceneBuilder = new SceneBuilder();
                 sceneBuilder.UpdateAll(scene);
@@ -210,13 +186,12 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                         Padding = new Thickness(11),
                         Child = border = new Border
                         {
-                            Opacity = 0.5,
                             Background = Brushes.Red,
                             Padding = new Thickness(12),
                             Child = canvas = new Canvas
                             {
-                                Opacity = 0.75,
-                            },
+                                Children = { new TextBlock() },
+                            }
                         }
                     }
                 };
@@ -224,6 +199,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
                 layout.ExecuteInitialLayoutPass(tree);
 
+                var animation = new BehaviorSubject<double>(0.5);
+                border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+                canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation);
+
                 var scene = new Scene(tree);
                 var sceneBuilder = new SceneBuilder();
                 sceneBuilder.UpdateAll(scene);
@@ -256,6 +235,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                         Child = border = new Border
                         {
                             Opacity = 0.5,
+                            Child = new Canvas(),
                         }
                     }
                 };
@@ -263,6 +243,9 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
                 var layout = AvaloniaLocator.Current.GetService<ILayoutManager>();
                 layout.ExecuteInitialLayoutPass(tree);
 
+                var animation = new BehaviorSubject<double>(0.5);
+                border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
+
                 var scene = new Scene(tree);
                 var sceneBuilder = new SceneBuilder();
                 sceneBuilder.UpdateAll(scene);