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);