diff --git a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject index a8c3abe8f2..04ab17c4e1 100644 --- a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject @@ -1,7 +1,6 @@  - 1000 - True + 3000 True \ No newline at end of file diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 2035b6f2aa..624274cc96 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -240,6 +240,19 @@ namespace Avalonia return (T)GetValue((AvaloniaProperty)property); } + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + public bool IsAnimating(AvaloniaProperty property) + { + Contract.Requires(property != null); + VerifyAccess(); + + return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; + } + /// /// Checks whether a is set on this object. /// diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index c11bab2236..c11f8ada7e 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -31,6 +31,13 @@ namespace Avalonia /// The value. T GetValue(AvaloniaProperty property); + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + bool IsAnimating(AvaloniaProperty property); + /// /// Checks whether a is set on this object. /// diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index cad86b5dea..6b82b202be 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -53,6 +53,18 @@ namespace Avalonia _validate = validate; } + /// + /// Gets a value indicating whether the property is animating. + /// + public bool IsAnimating + { + get + { + return ValuePriority <= (int)BindingPriority.Animation && + GetLevel(ValuePriority).ActiveBindingIndex != -1; + } + } + /// /// Gets the owner of the value. /// diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index f7befa646a..041d8f8f6b 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/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(renderTarget != null); _root = root; - _renderTarget = renderTarget; + RenderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layers = new RenderLayers(); + Layers = new RenderLayers(); } /// @@ -94,6 +92,16 @@ namespace Avalonia.Rendering /// public string DebugFramesPath { get; set; } + /// + /// Gets the render layers. + /// + internal RenderLayers Layers { get; } + + /// + /// Gets the current render target. + /// + internal IRenderTarget RenderTarget { get; private set; } + /// 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); diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 2d5a864089..84313f0906 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/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); } } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 707b31998a..eac362e997 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/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); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 0d2fc17b95..234cadbf31 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/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. /// /// The drawing context. - void BeginRender(IDrawingContextImpl context); + /// Whether to skip pushing the control's opacity. + void BeginRender(IDrawingContextImpl context, bool skipOpacity); /// /// Resets the drawing context after rendering the node's geometry. /// /// The drawing context. - void EndRender(IDrawingContextImpl context); + /// Whether to skip popping the control's opacity. + void EndRender(IDrawingContextImpl context, bool skipOpacity); /// /// Hit test the geometry in this node. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index b5ac7e4077..8f4f487e08 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/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; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index dd5740e4a9..6bea4d9bd6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -22,6 +22,7 @@ namespace Avalonia.Rendering.SceneGraph private List _children; private List _drawOperations; private bool _drawOperationsCloned; + private Matrix transformRestore; /// /// Initializes a new instance of the class. @@ -218,8 +219,10 @@ namespace Avalonia.Rendering.SceneGraph } /// - 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); + } } /// - 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() diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 4f8ba93c8f..c75150ca6d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/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(); + + 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("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + /// /// Returns an observable that returns a single value but does not complete. /// diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index 3a37585dc0..84ff492512 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/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("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index 352efe5358..d837f2cb2f 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/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(); diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index c413904c8f..b75b59c212 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/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(); diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs index f57ac836fb..82be755a39 100644 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ b/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(); diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs index 07a988f410..03b2f03bf2 100644 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ b/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(); diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 9ec053f075..dc137e3533 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/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( - x => x.CreateDrawingContext(It.IsAny()) == Mock.Of()); public TestRoot() { @@ -65,7 +63,21 @@ namespace Avalonia.UnitTests IStyleHost IStyleHost.StylingParent => StylingParent; - public IRenderTarget CreateRenderTarget() => _renderTarget; + public IRenderTarget CreateRenderTarget() + { + var dc = new Mock(); + dc.Setup(x => x.CreateLayer(It.IsAny())).Returns(() => + { + var layerDc = new Mock(); + var layer = new Mock(); + layer.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(layerDc.Object); + return layer.Object; + }); + + var result = new Mock(); + result.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(dc.Object); + return result.Object; + } public void Invalidate(Rect rect) { diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index cec95d4807..c97070a2aa 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/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(); var root = new TestRoot(); var dispatcher = new Mock(); dispatcher.Setup(x => x.InvokeAsync(It.IsAny(), DispatcherPriority.Render)) .Callback((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(a => a.Method.Name == "UpdateScene"), DispatcherPriority.Render)); -#endif } [Fact] @@ -49,15 +41,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering var loop = new Mock(); 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())); } @@ -68,12 +53,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering var loop = new Mock(); 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(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(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(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(); var root = new TestRoot(); var rootLayer = new Mock(); - var dispatcher = new ImmediateDispatcher(); var sceneBuilder = new Mock(); sceneBuilder.Setup(x => x.UpdateAll(It.IsAny())) @@ -143,23 +207,53 @@ namespace Avalonia.Visuals.UnitTests.Rendering }); var renderInterface = new Mock(); + 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(); + var target = CreateTargetAndRunFrame(root, loop: loop); + + Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot)); + + var animation = new BehaviorSubject(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(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())) - .Returns(rootLayer) - .Returns(borderLayer); - var loop = new Mock(); - 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(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(), It.IsAny(), It.IsAny()), 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(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(), It.IsAny())); + } + + private DeferredRenderer CreateTargetAndRunFrame( + TestRoot root, + Mock loop = null, + ISceneBuilder sceneBuilder = null, + IDispatcher dispatcher = null) + { + loop = loop ?? new Mock(); + 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(), It.IsAny(), It.IsAny()), Times.Never); + private Mock GetLayerContext(DeferredRenderer renderer, IControl layerRoot) + { + return Mock.Get(renderer.Layers[layerRoot].Bitmap.CreateDrawingContext(null)); } private void IgnoreFirstFrame(Mock loop, Mock sceneBuilder) diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index d0f7671956..44b9ebe8ce 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/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(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); sceneBuilder.UpdateAll(scene); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs index ed74f12075..f2d137249a 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs +++ b/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(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(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(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(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(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(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(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(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(); layout.ExecuteInitialLayoutPass(tree); + var animation = new BehaviorSubject(0.5); + border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation); + var scene = new Scene(tree); var sceneBuilder = new SceneBuilder(); sceneBuilder.UpdateAll(scene);